feat: use github client to generate markdown
parent
8c4815cd12
commit
e60e6852ad
|
@ -9,3 +9,6 @@
|
|||
|
||||
# VSCode
|
||||
.vscode
|
||||
|
||||
# GitHub
|
||||
.github_access_token
|
||||
|
|
|
@ -1,18 +1,36 @@
|
|||
<!doctype html><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><link rel=preconnect href=https://fonts.googleapis.com><link rel=preconnect href=https://fonts.gstatic.com crossorigin><link href="https://fonts.googleapis.com/css2?family=Recursive:wght,CASL,MONO@300..800,0..1,0..1&display=swap" rel=stylesheet><link href=https://haunt98.github.io/iosevka_webfont/iosevka-term-ss08/iosevka-term-ss08.css rel=stylesheet><link rel=stylesheet href=styles.css><a href=index>Index</a><h1>Backup my way</h1><p>First thing first, I want to list my own devices, which I have through the years:<ul><li><del>Laptop Samsung NP300E4Z-S06VN (Old laptop which I give to my mom)</del><li><del><a href=https://www.dell.com/support/home/en-vn/product-support/product/inspiron-15-3567-laptop/drivers>Laptop Dell Inspiron 15 3567</a> (My mom bought it for me when I go to college, I give it to my mom afterward)</del><li><del><a href=https://www.acer.com/ac/en/US/content/support-product/8841>Laptop Acer Nitro AN515-45</a> (Gaming laptop which I buy for gaming, I give it to my sister)</del><li>MacBook Pro M1 2020 (My company laptop)<li><del>Phone <a href=https://forum.xda-developers.com/c/lg-g3.3147/>LG G3</a> (Bought long time ago, now is a brick)</del><li><del>Phone <a href=https://forum.xda-developers.com/c/xiaomi-redmi-6a.7881/>Xiaomi Redmi 6A</a> (I give it to my sister too)</del><li>Phone <a href=https://forum.xda-developers.com/c/xiaomi-poco-x3-nfc.11523/>Xiaomi Poco X3 NFC</a> (Primary phone which I use daily)</ul><p>App/Service I use daily:<ul><li><a href=https://bitwarden.com/>Bitwarden</a><li><a href=https://getaegis.app/>Aegis Authenticator</a><li><a href=https://rclone.org/>Rclone</a><li><a href=https://restic.net/>restic</a><li><a href=https://tailscale.com/>Tailscale</a><li>GitHub / GitLab<li>Google Keep / Notion<li>Google Drive (I use 200GB plan)</ul><p>The purpose is that I want my data to be safe, secure, and can be easily recovered if I lost some devices;<br>or in the worst situation, I lost all.<br>Because you know, it is hard to guess what is waiting for us in the future.<p>There are 2 sections which I want to share, the first is <strong>How to backup</strong>, the second is <strong>Recover strategy</strong>.<h2>How to backup</h2><p>Before I talk about backup, I want to talk about data.<br>In specifically, which data should I backup?<p>I use Arch Linux and macOS, primarily work in the terminal so I have too many dotfiles, for example, <code>~/.config/nvim/init.lua</code>.<br>Each time I reinstall Arch Linux (I like it a lot), I need to reconfigure all the settings, and it is time-consuming.<p>So for the DE and UI settings, I keep it as default as possible, unless it's getting in my way, I leave the default setting there and forget about it.<br>The others are dotfiles, which I write my own <a href=https://github.com/haunt98/dotfiles>dotfiles tool</a> to backup and reconfigure easily and quickly.<br>Also, I know that installing Arch Linux is not easy, despite I install it too many times (Like thousand times since I was in high school).<br>Not because it is hard, but as life goes on, the <a href=https://wiki.archlinux.org/title/installation_guide>official install guide</a> keeps getting new update and covering too many cases for my own personal use, so I write my own <a href=https://github.com/haunt98/til/blob/main/install-archlinux.md>guide</a> to quickly capture what I need to do.<br>I back up all my dotfiles in GitHub and GitLab as I trust them both.<br>Also as I travel the Internet, I discover <a href=https://codeberg.org/>Codeberg</a> and <a href=https://gitea.treehouse.systems/>Treehouse</a> and use them as another backup for git repo.<p>So that is my dotfiles, for my regular data, like Wallpaper or Books, Images, I use Google Drive (Actually I pay for it).<br>But the step: open the webpage, click the upload button and choose files seems boring and time-consuming.<br>So I use Rclone, it supports Google Drive, One Drive and many providers but I only use Google Drive for now.<br>The commands are simple:<pre><code class=language-sh># Sync from local to remote
|
||||
<!doctype html><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><link rel=stylesheet href=https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.1.0/github-markdown-dark.css><style>.markdown-body{box-sizing:border-box;min-width:200px;max-width:980px;margin:0 auto;padding:45px}@media(max-width:767px){.markdown-body{padding:15px}}</style><body class=markdown-body><a href=index>Index</a><h1><a id=user-content-backup-my-way class=anchor aria-hidden=true href=#backup-my-way><span aria-hidden=true class="octicon octicon-link"></span></a>Backup my way</h1><p>First thing first, I want to list my own devices, which I have through the years:<ul><li><del>Laptop Samsung NP300E4Z-S06VN (Old laptop which I give to my mom)</del><li><del><a href=https://www.dell.com/support/home/en-vn/product-support/product/inspiron-15-3567-laptop/drivers rel=nofollow>Laptop Dell Inspiron 15 3567</a> (My mom bought it for me when I go to college, I give it to my mom afterward)</del><li><del><a href=https://www.acer.com/ac/en/US/content/support-product/8841 rel=nofollow>Laptop Acer Nitro AN515-45</a> (Gaming laptop which I buy for gaming, I give it to my sister)</del><li>MacBook Pro M1 2020 (My company laptop)<li><del>Phone <a href=https://forum.xda-developers.com/c/lg-g3.3147/ rel=nofollow>LG G3</a> (Bought long time ago, now is a brick)</del><li><del>Phone <a href=https://forum.xda-developers.com/c/xiaomi-redmi-6a.7881/ rel=nofollow>Xiaomi Redmi 6A</a> (I give it to my sister too)</del><li>Phone <a href=https://forum.xda-developers.com/c/xiaomi-poco-x3-nfc.11523/ rel=nofollow>Xiaomi Poco X3 NFC</a> (Primary phone which I use daily)</ul><p>App/Service I use daily:<ul><li><a href=https://bitwarden.com/ rel=nofollow>Bitwarden</a><li><a href=https://getaegis.app/ rel=nofollow>Aegis Authenticator</a><li><a href=https://rclone.org/ rel=nofollow>Rclone</a><li><a href=https://restic.net/ rel=nofollow>restic</a><li><a href=https://tailscale.com/ rel=nofollow>Tailscale</a><li>GitHub / GitLab<li>Google Keep / Notion<li>Google Drive (I use 200GB plan)</ul><p>The purpose is that I want my data to be safe, secure, and can be easily recovered if I lost some devices;
|
||||
or in the worst situation, I lost all.
|
||||
Because you know, it is hard to guess what is waiting for us in the future.<p>There are 2 sections which I want to share, the first is <strong>How to backup</strong>, the second is <strong>Recover strategy</strong>.<h2><a id=user-content-how-to-backup class=anchor aria-hidden=true href=#how-to-backup><span aria-hidden=true class="octicon octicon-link"></span></a>How to backup</h2><p>Before I talk about backup, I want to talk about data.
|
||||
In specifically, which data should I backup?<p>I use Arch Linux and macOS, primarily work in the terminal so I have too many dotfiles, for example, <code>~/.config/nvim/init.lua</code>.
|
||||
Each time I reinstall Arch Linux (I like it a lot), I need to reconfigure all the settings, and it is time-consuming.<p>So for the DE and UI settings, I keep it as default as possible, unless it's getting in my way, I leave the default setting there and forget about it.
|
||||
The others are dotfiles, which I write my own <a href=https://github.com/haunt98/dotfiles>dotfiles tool</a> to backup and reconfigure easily and quickly.
|
||||
Also, I know that installing Arch Linux is not easy, despite I install it too many times (Like thousand times since I was in high school).
|
||||
Not because it is hard, but as life goes on, the <a href=https://wiki.archlinux.org/title/installation_guide rel=nofollow>official install guide</a> keeps getting new update and covering too many cases for my own personal use, so I write my own <a href=https://github.com/haunt98/til/blob/main/install-archlinux.md>guide</a> to quickly capture what I need to do.
|
||||
I back up all my dotfiles in GitHub and GitLab as I trust them both.
|
||||
Also as I travel the Internet, I discover <a href=https://codeberg.org/ rel=nofollow>Codeberg</a> and <a href=https://gitea.treehouse.systems/ rel=nofollow>Treehouse</a> and use them as another backup for git repo.<p>So that is my dotfiles, for my regular data, like Wallpaper or Books, Images, I use Google Drive (Actually I pay for it).
|
||||
But the step: open the webpage, click the upload button and choose files seems boring and time-consuming.
|
||||
So I use Rclone, it supports Google Drive, One Drive and many providers but I only use Google Drive for now.
|
||||
The commands are simple:<div class="highlight highlight-source-shell"><pre><span class=pl-c><span class=pl-c>#</span> Sync from local to remote</span>
|
||||
rclone sync MyBooks remote:MyBooks -P --exclude .DS_Store
|
||||
|
||||
# Sync from remote to local
|
||||
rclone sync remote:MyBooks MyBooks -P --exclude .DS_Store
|
||||
</code></pre><p>Before you use Rclone to sync to Google Drive, you should read <a href=https://rclone.org/drive/>Google Drive rclone configuration</a> first.<p>For private data, I use restic which can be used with Rclone:<pre><code class=language-sh># Init
|
||||
<span class=pl-c><span class=pl-c>#</span> Sync from remote to local</span>
|
||||
rclone sync remote:MyBooks MyBooks -P --exclude .DS_Store</pre></div><p>Before you use Rclone to sync to Google Drive, you should read <a href=https://rclone.org/drive/ rel=nofollow>Google Drive rclone configuration</a> first.<p>For private data, I use restic which can be used with Rclone:<div class="highlight highlight-source-shell"><pre><span class=pl-c><span class=pl-c>#</span> Init</span>
|
||||
restic -r rclone:remote:PrivateData init
|
||||
|
||||
# Backup
|
||||
<span class=pl-c><span class=pl-c>#</span> Backup</span>
|
||||
restic -r rclone:remote:PrivateData backup PrivateData
|
||||
|
||||
# Cleanup old backups
|
||||
<span class=pl-c><span class=pl-c>#</span> Cleanup old backups</span>
|
||||
restic -r rclone:remote:PrivateData forget --keep-last 1 --prune
|
||||
|
||||
# Restore
|
||||
restic -r rclone:remote:PrivateData restore latest --target ~
|
||||
</code></pre><p>The next data is my passwords and my OTPs.<br>These are the things which I'm scare to lose the most.<br>First thing first, I enable 2-Step Verification for all of my important accounts, should use both OTP and phone method.<p>I use Bitwarden for passwords (That is a long story, coming from Google Password manager to Firefox Lockwise and then settle down with Bitwarden) and Aegis for OTPs.<br>The reason I choose Aegis, not Authy (I use Authy for so long but Aegis is definitely better) is because Aegis allows me to extract all the OTPs to a single file (Can be encrypted), which I use to transfer or backup easily.<p>As long as Bitwarden provides free passwords stored, I use all of its apps, extensions so that I can easily sync passwords between laptops and phones.<br>The thing I need to remember is the master password of Bitwarden in my head.<p>With Aegis, I export the data, then sync it to Google Drive, also store it locally in my phone.<p>The main problem here is the OTP, I can not store all of my OTPs in the cloud completely.<br>Because if I want to access my OTPs in the cloud, I should log in, and then input my OTP, this is a circle, my friends.<h2>Recovery strategy</h2><p>There are many strategies that I process to react as if something strange is happening to my devices.<p>If I lost my laptops, single laptop or all, do not panic as long as I have my phones.<br>The OTPs are in there, the passwords are in Bitwarden cloud, other data is in Google Drive so nothing is lost here.<p>If I lost my phone, but not my laptops, I use the OTPs which are stored locally in my laptops.<p>In the worst situation, I lost everything, my laptops, my phone.<br>The first step is to recover my SIM, then log in to Google account using the password and SMS OTP.<br>After that, log in to Bitwarden account using the master password and OTP from Gmail, which I open previously.<h2>The end</h2><p>This guide will be updated regularly I promise.</p><a href=mailto:hauvipapro+posts@gmail.com>Feel free to ask me via email</a>
|
||||
<span class=pl-c><span class=pl-c>#</span> Restore</span>
|
||||
restic -r rclone:remote:PrivateData restore latest --target <span class=pl-k>~</span></pre></div><p>The next data is my passwords and my OTPs.
|
||||
These are the things which I'm scare to lose the most.
|
||||
First thing first, I enable 2-Step Verification for all of my important accounts, should use both OTP and phone method.<p>I use Bitwarden for passwords (That is a long story, coming from Google Password manager to Firefox Lockwise and then settle down with Bitwarden) and Aegis for OTPs.
|
||||
The reason I choose Aegis, not Authy (I use Authy for so long but Aegis is definitely better) is because Aegis allows me to extract all the OTPs to a single file (Can be encrypted), which I use to transfer or backup easily.<p>As long as Bitwarden provides free passwords stored, I use all of its apps, extensions so that I can easily sync passwords between laptops and phones.
|
||||
The thing I need to remember is the master password of Bitwarden in my head.<p>With Aegis, I export the data, then sync it to Google Drive, also store it locally in my phone.<p>The main problem here is the OTP, I can not store all of my OTPs in the cloud completely.
|
||||
Because if I want to access my OTPs in the cloud, I should log in, and then input my OTP, this is a circle, my friends.<h2><a id=user-content-recovery-strategy class=anchor aria-hidden=true href=#recovery-strategy><span aria-hidden=true class="octicon octicon-link"></span></a>Recovery strategy</h2><p>There are many strategies that I process to react as if something strange is happening to my devices.<p>If I lost my laptops, single laptop or all, do not panic as long as I have my phones.
|
||||
The OTPs are in there, the passwords are in Bitwarden cloud, other data is in Google Drive so nothing is lost here.<p>If I lost my phone, but not my laptops, I use the OTPs which are stored locally in my laptops.<p>In the worst situation, I lost everything, my laptops, my phone.
|
||||
The first step is to recover my SIM, then log in to Google account using the password and SMS OTP.
|
||||
After that, log in to Bitwarden account using the master password and OTP from Gmail, which I open previously.<h2><a id=user-content-the-end class=anchor aria-hidden=true href=#the-end><span aria-hidden=true class="octicon octicon-link"></span></a>The end</h2><p>This guide will be updated regularly I promise.</p><a href=mailto:hauvipapro+posts@gmail.com>Feel free to ask me via email</a>
|
||||
<a rel=me href=https://hachyderm.io/@haunguyen>Mastodon</a>
|
|
@ -1,36 +1,44 @@
|
|||
<!doctype html><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><link rel=preconnect href=https://fonts.googleapis.com><link rel=preconnect href=https://fonts.gstatic.com crossorigin><link href="https://fonts.googleapis.com/css2?family=Recursive:wght,CASL,MONO@300..800,0..1,0..1&display=swap" rel=stylesheet><link href=https://haunt98.github.io/iosevka_webfont/iosevka-term-ss08/iosevka-term-ss08.css rel=stylesheet><link rel=stylesheet href=styles.css><a href=index>Index</a><h1>Dockerfile for Go</h1><p>Each time I start a new Go project, I repeat many steps.<br>Like set up <code>.gitignore</code>, CI configs, Dockerfile, ...<p>So I decide to have a baseline Dockerfile like this:<pre><code class=language-Dockerfile>FROM golang:1.19-bullseye as builder
|
||||
<!doctype html><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><link rel=stylesheet href=https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.1.0/github-markdown-dark.css><style>.markdown-body{box-sizing:border-box;min-width:200px;max-width:980px;margin:0 auto;padding:45px}@media(max-width:767px){.markdown-body{padding:15px}}</style><body class=markdown-body><a href=index>Index</a><h1><a id=user-content-dockerfile-for-go class=anchor aria-hidden=true href=#dockerfile-for-go><span aria-hidden=true class="octicon octicon-link"></span></a>Dockerfile for Go</h1><p>Each time I start a new Go project, I repeat many steps.
|
||||
Like set up <code>.gitignore</code>, CI configs, Dockerfile, ...<p>So I decide to have a baseline Dockerfile like this:<div class="highlight highlight-source-dockerfile"><pre><span class=pl-k>FROM</span> golang:1.19-bullseye as builder
|
||||
|
||||
RUN go install golang.org/dl/go1.19@latest \
|
||||
<span class=pl-k>RUN</span> go install golang.org/dl/go1.19@latest \
|
||||
&& go1.19 download
|
||||
|
||||
WORKDIR /build
|
||||
<span class=pl-k>WORKDIR</span> /build
|
||||
|
||||
COPY go.mod .
|
||||
COPY go.sum .
|
||||
COPY vendor .
|
||||
COPY . .
|
||||
<span class=pl-k>COPY</span> go.mod .
|
||||
<span class=pl-k>COPY</span> go.sum .
|
||||
<span class=pl-k>COPY</span> vendor .
|
||||
<span class=pl-k>COPY</span> . .
|
||||
|
||||
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GOAMD64=v3 go build -o ./app -tags timetzdata -trimpath .
|
||||
<span class=pl-k>RUN</span> CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GOAMD64=v3 go build -o ./app -tags timetzdata -trimpath .
|
||||
|
||||
FROM gcr.io/distroless/base-debian11
|
||||
<span class=pl-k>FROM</span> gcr.io/distroless/base-debian11
|
||||
|
||||
COPY --from=builder /build/app /app
|
||||
<span class=pl-k>COPY</span> --from=builder /build/app /app
|
||||
|
||||
ENTRYPOINT ["/app"]
|
||||
</code></pre><p>I use <a href=https://docs.docker.com/develop/develop-images/multistage-build/>multi-stage build</a> to keep my image size small.<br>First stage is <a href=https://hub.docker.com/_/golang>Go official image</a>,<br>second stage is <a href=https://github.com/GoogleContainerTools/distroless>Distroless</a>.<p>Before Distroless, I use <a href=https://hub.docker.com/_/alpine>Alpine official image</a>,<br>There is a whole discussion on the Internet to choose which is the best base image for Go.<br>After reading some blogs, I discover Distroless as a small and secure base image.<br>So I stick with it for a while.<p>Also, remember to match Distroless Debian version with Go official image Debian version.<pre><code class=language-Dockerfile>FROM golang:1.19-bullseye as builder
|
||||
</code></pre><p>This is Go image I use as a build stage.<br>This can be official Go image or custom image is required in some companies.<pre><code class=language-Dockerfile>RUN go install golang.org/dl/go1.19@latest \
|
||||
&& go1.19 download
|
||||
</code></pre><p>This is optional.<br>In my case, my company is slow to update Go image so I use this trick to install latest Go version.<pre><code class=language-Dockerfile>WORKDIR /build
|
||||
<span class=pl-k>ENTRYPOINT</span> [<span class=pl-s>"/app"</span>]</pre></div><p>I use <a href=https://docs.docker.com/develop/develop-images/multistage-build/ rel=nofollow>multi-stage build</a> to keep my image size small.
|
||||
First stage is <a href=https://hub.docker.com/_/golang rel=nofollow>Go official image</a>,
|
||||
second stage is <a href=https://github.com/GoogleContainerTools/distroless>Distroless</a>.<p>Before Distroless, I use <a href=https://hub.docker.com/_/alpine rel=nofollow>Alpine official image</a>,
|
||||
There is a whole discussion on the Internet to choose which is the best base image for Go.
|
||||
After reading some blogs, I discover Distroless as a small and secure base image.
|
||||
So I stick with it for a while.<p>Also, remember to match Distroless Debian version with Go official image Debian version.<div class="highlight highlight-source-dockerfile"><pre><span class=pl-k>FROM</span> golang:1.19-bullseye as builder</pre></div><p>This is Go image I use as a build stage.
|
||||
This can be official Go image or custom image is required in some companies.<div class="highlight highlight-source-dockerfile"><pre><span class=pl-k>RUN</span> go install golang.org/dl/go1.19@latest \
|
||||
&& go1.19 download</pre></div><p>This is optional.
|
||||
In my case, my company is slow to update Go image so I use this trick to install latest Go version.<div class="highlight highlight-source-dockerfile"><pre><span class=pl-k>WORKDIR</span> /build
|
||||
|
||||
COPY go.mod .
|
||||
COPY go.sum .
|
||||
COPY vendor .
|
||||
COPY . .
|
||||
</code></pre><p>I use <code>/build</code> to emphasize that I am building something in that directory.<p>The 4 <code>COPY</code> lines are familiar if you use Go enough.<br>First is <code>go.mod</code> and <code>go.sum</code> because it defines Go modules.<br>The second is <code>vendor</code>, this is optional but I use it because I don't want each time I build Dockerfile, I need to redownload Go modules.<pre><code class=language-Dockerfile>RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GOAMD64=v3 go build -o ./app -tags timetzdata -trimpath .
|
||||
</code></pre><p>This is where I build Go program.<p><code>CGO_ENABLED=0</code> because I don't want to mess with C libraries.<br><code>GOOS=linux GOARCH=amd64</code> is easy to explain, Linux with x86-64.<br><code>GOAMD64=v3</code> is new since <a href=https://go.dev/doc/go1.18#amd64>Go 1.18</a>,<br>I use v3 because I read about AMD64 version in <a href=https://gitlab.archlinux.org/archlinux/rfcs/-/blob/master/rfcs/0002-march.rst>Arch Linux rfcs</a>. TLDR's newer computers are already x86-64-v3.<p><code>-tags timetzdata</code> to embed timezone database incase base image does not have.<br><code>-trimpath</code> to support reproduce build.<pre><code class=language-Dockerfile>FROM gcr.io/distroless/base-debian11
|
||||
<span class=pl-k>COPY</span> go.mod .
|
||||
<span class=pl-k>COPY</span> go.sum .
|
||||
<span class=pl-k>COPY</span> vendor .
|
||||
<span class=pl-k>COPY</span> . .</pre></div><p>I use <code>/build</code> to emphasize that I am building something in that directory.<p>The 4 <code>COPY</code> lines are familiar if you use Go enough.
|
||||
First is <code>go.mod</code> and <code>go.sum</code> because it defines Go modules.
|
||||
The second is <code>vendor</code>, this is optional but I use it because I don't want each time I build Dockerfile, I need to redownload Go modules.<div class="highlight highlight-source-dockerfile"><pre><span class=pl-k>RUN</span> CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GOAMD64=v3 go build -o ./app -tags timetzdata -trimpath .</pre></div><p>This is where I build Go program.<p><code>CGO_ENABLED=0</code> because I don't want to mess with C libraries.
|
||||
<code>GOOS=linux GOARCH=amd64</code> is easy to explain, Linux with x86-64.
|
||||
<code>GOAMD64=v3</code> is new since <a href=https://go.dev/doc/go1.18#amd64 rel=nofollow>Go 1.18</a>,
|
||||
I use v3 because I read about AMD64 version in <a href=https://gitlab.archlinux.org/archlinux/rfcs/-/blob/master/rfcs/0002-march.rst rel=nofollow>Arch Linux rfcs</a>. TLDR's newer computers are already x86-64-v3.<p><code>-tags timetzdata</code> to embed timezone database incase base image does not have.
|
||||
<code>-trimpath</code> to support reproduce build.<div class="highlight highlight-source-dockerfile"><pre><span class=pl-k>FROM</span> gcr.io/distroless/base-debian11
|
||||
|
||||
COPY --from=builder /build/app /app
|
||||
<span class=pl-k>COPY</span> --from=builder /build/app /app
|
||||
|
||||
ENTRYPOINT ["/app"]
|
||||
</code></pre><p>Finally, I copy <code>app</code> to Distroless base image.</p><a href=mailto:hauvipapro+posts@gmail.com>Feel free to ask me via email</a>
|
||||
<span class=pl-k>ENTRYPOINT</span> [<span class=pl-s>"/app"</span>]</pre></div><p>Finally, I copy <code>app</code> to Distroless base image.</p><a href=mailto:hauvipapro+posts@gmail.com>Feel free to ask me via email</a>
|
||||
<a rel=me href=https://hachyderm.io/@haunguyen>Mastodon</a>
|
|
@ -1,99 +1,113 @@
|
|||
<!doctype html><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><link rel=preconnect href=https://fonts.googleapis.com><link rel=preconnect href=https://fonts.gstatic.com crossorigin><link href="https://fonts.googleapis.com/css2?family=Recursive:wght,CASL,MONO@300..800,0..1,0..1&display=swap" rel=stylesheet><link href=https://haunt98.github.io/iosevka_webfont/iosevka-term-ss08/iosevka-term-ss08.css rel=stylesheet><link rel=stylesheet href=styles.css><a href=index>Index</a><h1>Bootstrap Go</h1><p>It is hard to write bootstrap tool to quickly create Go service.<br>So I write this guide instead.<br>This is a quick checklist for me every damn time I need to write a Go service from scratch.<br>Also, this is my personal opinion, so feel free to comment.<h2>Structure</h2><pre><code class=language-txt>main.go
|
||||
<!doctype html><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><link rel=stylesheet href=https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.1.0/github-markdown-dark.css><style>.markdown-body{box-sizing:border-box;min-width:200px;max-width:980px;margin:0 auto;padding:45px}@media(max-width:767px){.markdown-body{padding:15px}}</style><body class=markdown-body><a href=index>Index</a><h1><a id=user-content-bootstrap-go class=anchor aria-hidden=true href=#bootstrap-go><span aria-hidden=true class="octicon octicon-link"></span></a>Bootstrap Go</h1><p>It is hard to write bootstrap tool to quickly create Go service.
|
||||
So I write this guide instead.
|
||||
This is a quick checklist for me every damn time I need to write a Go service from scratch.
|
||||
Also, this is my personal opinion, so feel free to comment.<h2><a id=user-content-structure class=anchor aria-hidden=true href=#structure><span aria-hidden=true class="octicon octicon-link"></span></a>Structure</h2><div class="highlight highlight-text-adblock"><pre>main.go
|
||||
internal
|
||||
| business
|
||||
| | http
|
||||
| | | handler.go
|
||||
| | | service.go
|
||||
| | | models.go
|
||||
| | grpc
|
||||
| | | handler.go
|
||||
| | | models.go
|
||||
| | consumer
|
||||
| | | handler.go
|
||||
| | | service.go
|
||||
| | | models.go
|
||||
| | service.go
|
||||
| | repository.go
|
||||
| | models.go
|
||||
</code></pre><p>All business codes are inside <code>internal</code>.<br>Each business has a different directory <code>business</code>.<p>Inside each business, there are 2 handlers: <code>http</code>, <code>grpc</code>:<ul><li><code>http</code> is for public APIs (Android, iOS, ... are clients).<li><code>grpc</code> is for internal APIs (other services are clients).<li><code>consumer</code> is for consuming messages from queue (Kafka, RabbitMQ, ...).</ul><p>For each handler, there are usually 3 layers: <code>handler</code>, <code>service</code>, <code>repository</code>:<ul><li><code>handler</code> interacts directly with gRPC, REST or consumer using specific codes (cookies, ...) In case gRPC, there are frameworks outside handle for us so we can write business/logic codes here too. But remember, gRPC only.<li><code>service</code> is where we write business/logic codes, and only business/logic codes is written here.<li><code>repository</code> is where we write codes which interacts with database/cache like MySQL, Redis, ...<li><code>models</code> is where we put all request, response, data models.</ul><p>Location:<ul><li><code>handler</code> must exist inside <code>grpc</code>, <code>http</code>, <code>consumer</code>.<li><code>service</code>, <code>models</code> can exist directly inside of <code>business</code> if both <code>grpc</code>, <code>http</code>, <code>consumer</code> has same business/logic.<li><code>repository</code> should be placed directly inside of <code>business</code>.</ul><h2>Do not repeat!</h2><p>If we have too many services, some of the logic will be overlapped.<p>For example, service A and service B both need to make POST call API to service C.<br>If service A and service B both have libs to call service C to do that API, we need to move the libs to some common pkg libs.<br>So in the future, service D which needs to call C will not need to copy libs to handle service C api but only need to import from common pkg libs.<p>Another bad practice is adapter service.<br>No need to write a new service if what we need is just common pkg libs.<h2>Taste on style guide</h2><h3>Stop using global var</h3><p>If I see someone using global var, I swear I will shoot them twice in the face.<p>Why?<ul><li>Can not write unit test.<li>Is not thread safe.</ul><h3>Use functional options, but don't overuse it!</h3><p>For simple struct with 1 or 2 fields, no need to use functional options.<p><a href=https://go.dev/play/p/0XnOLiHuoz3>Example</a>:<pre><code class=language-go>func main() {
|
||||
s := NewS(WithA(1), WithB("b"))
|
||||
fmt.Printf("%+v\n", s)
|
||||
<span class=pl-k>|</span> business
|
||||
<span class=pl-k>|</span> | http
|
||||
<span class=pl-k>|</span> | | handler.go
|
||||
<span class=pl-k>|</span> | | service.go
|
||||
<span class=pl-k>|</span> | | models.go
|
||||
<span class=pl-k>|</span> | grpc
|
||||
<span class=pl-k>|</span> | | handler.go
|
||||
<span class=pl-k>|</span> | | models.go
|
||||
<span class=pl-k>|</span> | consumer
|
||||
<span class=pl-k>|</span> | | handler.go
|
||||
<span class=pl-k>|</span> | | service.go
|
||||
<span class=pl-k>|</span> | | models.go
|
||||
<span class=pl-k>|</span> | service.go
|
||||
<span class=pl-k>|</span> | repository.go
|
||||
<span class=pl-k>|</span> | models.go</pre></div><p>All business codes are inside <code>internal</code>.
|
||||
Each business has a different directory <code>business</code>.<p>Inside each business, there are 2 handlers: <code>http</code>, <code>grpc</code>:<ul><li><code>http</code> is for public APIs (Android, iOS, ... are clients).<li><code>grpc</code> is for internal APIs (other services are clients).<li><code>consumer</code> is for consuming messages from queue (Kafka, RabbitMQ, ...).</ul><p>For each handler, there are usually 3 layers: <code>handler</code>, <code>service</code>, <code>repository</code>:<ul><li><code>handler</code> interacts directly with gRPC, REST or consumer using specific codes (cookies, ...) In case gRPC, there are frameworks outside handle for us so we can write business/logic codes here too. But remember, gRPC only.<li><code>service</code> is where we write business/logic codes, and only business/logic codes is written here.<li><code>repository</code> is where we write codes which interacts with database/cache like MySQL, Redis, ...<li><code>models</code> is where we put all request, response, data models.</ul><p>Location:<ul><li><code>handler</code> must exist inside <code>grpc</code>, <code>http</code>, <code>consumer</code>.<li><code>service</code>, <code>models</code> can exist directly inside of <code>business</code> if both <code>grpc</code>, <code>http</code>, <code>consumer</code> has same business/logic.<li><code>repository</code> should be placed directly inside of <code>business</code>.</ul><h2><a id=user-content-do-not-repeat class=anchor aria-hidden=true href=#do-not-repeat><span aria-hidden=true class="octicon octicon-link"></span></a>Do not repeat!</h2><p>If we have too many services, some of the logic will be overlapped.<p>For example, service A and service B both need to make POST call API to service C.
|
||||
If service A and service B both have libs to call service C to do that API, we need to move the libs to some common pkg libs.
|
||||
So in the future, service D which needs to call C will not need to copy libs to handle service C api but only need to import from common pkg libs.<p>Another bad practice is adapter service.
|
||||
No need to write a new service if what we need is just common pkg libs.<h2><a id=user-content-taste-on-style-guide class=anchor aria-hidden=true href=#taste-on-style-guide><span aria-hidden=true class="octicon octicon-link"></span></a>Taste on style guide</h2><h3><a id=user-content-stop-using-global-var class=anchor aria-hidden=true href=#stop-using-global-var><span aria-hidden=true class="octicon octicon-link"></span></a>Stop using global var</h3><p>If I see someone using global var, I swear I will shoot them twice in the face.<p>Why?<ul><li>Can not write unit test.<li>Is not thread safe.</ul><h3><a id=user-content-use-functional-options-but-dont-overuse-it class=anchor aria-hidden=true href=#use-functional-options-but-dont-overuse-it><span aria-hidden=true class="octicon octicon-link"></span></a>Use functional options, but don't overuse it!</h3><p>For simple struct with 1 or 2 fields, no need to use functional options.<p><a href=https://go.dev/play/p/0XnOLiHuoz3 rel=nofollow>Example</a>:<div class="highlight highlight-source-go"><pre><span class=pl-k>func</span> <span class=pl-en>main</span>() {
|
||||
<span class=pl-s1>s</span> <span class=pl-c1>:=</span> <span class=pl-en>NewS</span>(<span class=pl-en>WithA</span>(<span class=pl-c1>1</span>), <span class=pl-en>WithB</span>(<span class=pl-s>"b"</span>))
|
||||
<span class=pl-s1>fmt</span>.<span class=pl-en>Printf</span>(<span class=pl-s>"%+v<span class=pl-cce>\n</span>"</span>, <span class=pl-s1>s</span>)
|
||||
}
|
||||
|
||||
type S struct {
|
||||
fieldA int
|
||||
fieldB string
|
||||
<span class=pl-k>type</span> <span class=pl-smi>S</span> <span class=pl-k>struct</span> {
|
||||
<span class=pl-c1>fieldA</span> <span class=pl-smi>int</span>
|
||||
<span class=pl-c1>fieldB</span> <span class=pl-smi>string</span>
|
||||
}
|
||||
|
||||
type OptionS func(s *S)
|
||||
<span class=pl-k>type</span> <span class=pl-smi>OptionS</span> <span class=pl-k>func</span>(<span class=pl-s1>s</span> <span class=pl-c1>*</span><span class=pl-smi>S</span>)
|
||||
|
||||
func WithA(a int) OptionS {
|
||||
return func(s *S) {
|
||||
s.fieldA = a
|
||||
<span class=pl-k>func</span> <span class=pl-en>WithA</span>(<span class=pl-s1>a</span> <span class=pl-smi>int</span>) <span class=pl-smi>OptionS</span> {
|
||||
<span class=pl-k>return</span> <span class=pl-k>func</span>(<span class=pl-s1>s</span> <span class=pl-c1>*</span><span class=pl-smi>S</span>) {
|
||||
<span class=pl-s1>s</span>.<span class=pl-c1>fieldA</span> <span class=pl-c1>=</span> <span class=pl-s1>a</span>
|
||||
}
|
||||
}
|
||||
|
||||
func WithB(b string) OptionS {
|
||||
return func(s *S) {
|
||||
s.fieldB = b
|
||||
<span class=pl-k>func</span> <span class=pl-en>WithB</span>(<span class=pl-s1>b</span> <span class=pl-smi>string</span>) <span class=pl-smi>OptionS</span> {
|
||||
<span class=pl-k>return</span> <span class=pl-k>func</span>(<span class=pl-s1>s</span> <span class=pl-c1>*</span><span class=pl-smi>S</span>) {
|
||||
<span class=pl-s1>s</span>.<span class=pl-c1>fieldB</span> <span class=pl-c1>=</span> <span class=pl-s1>b</span>
|
||||
}
|
||||
}
|
||||
|
||||
func NewS(opts ...OptionS) *S {
|
||||
s := &S{}
|
||||
for _, opt := range opts {
|
||||
opt(s)
|
||||
<span class=pl-k>func</span> <span class=pl-en>NewS</span>(<span class=pl-s1>opts</span> <span class=pl-c1>...</span><span class=pl-smi>OptionS</span>) <span class=pl-c1>*</span><span class=pl-smi>S</span> {
|
||||
<span class=pl-s1>s</span> <span class=pl-c1>:=</span> <span class=pl-c1>&</span><span class=pl-smi>S</span>{}
|
||||
<span class=pl-k>for</span> <span class=pl-s1>_</span>, <span class=pl-s1>opt</span> <span class=pl-c1>:=</span> <span class=pl-k>range</span> <span class=pl-s1>opts</span> {
|
||||
<span class=pl-en>opt</span>(<span class=pl-s1>s</span>)
|
||||
}
|
||||
return s
|
||||
}
|
||||
</code></pre><p>In above example, I construct <code>s</code> with <code>WithA</code> and <code>WithB</code> option.<br>No need to pass direct field inside <code>s</code>.<h3>Use <a href=https://pkg.go.dev/golang.org/x/sync/errgroup>errgroup</a> as much as possible</h3><p>If business logic involves calling too many APIs, but they are not depend on each other.<br>We can fire them parallel :)<p>Personally, I prefer <code>errgroup</code> to <code>WaitGroup</code> (<a href=https://pkg.go.dev/sync#WaitGroup)>https://pkg.go.dev/sync#WaitGroup)</a>.<br>Because I always need deal with error.<p>Example:<pre><code class=language-go>eg, egCtx := errgroup.WithContext(ctx)
|
||||
<span class=pl-k>return</span> <span class=pl-s1>s</span>
|
||||
}</pre></div><p>In above example, I construct <code>s</code> with <code>WithA</code> and <code>WithB</code> option.
|
||||
No need to pass direct field inside <code>s</code>.<h3><a id=user-content-use-errgroup-as-much-as-possible class=anchor aria-hidden=true href=#use-errgroup-as-much-as-possible><span aria-hidden=true class="octicon octicon-link"></span></a>Use <a href=https://pkg.go.dev/golang.org/x/sync/errgroup rel=nofollow>errgroup</a> as much as possible</h3><p>If business logic involves calling too many APIs, but they are not depend on each other.
|
||||
We can fire them parallel :)<p>Personally, I prefer <code>errgroup</code> to <code>WaitGroup</code> (<a href=https://pkg.go.dev/sync#WaitGroup rel=nofollow>https://pkg.go.dev/sync#WaitGroup</a>).
|
||||
Because I always need deal with error.<p>Example:<div class="highlight highlight-source-go"><pre><span class=pl-s1>eg</span>, <span class=pl-s1>egCtx</span> <span class=pl-c1>:=</span> <span class=pl-s1>errgroup</span>.<span class=pl-en>WithContext</span>(<span class=pl-s1>ctx</span>)
|
||||
|
||||
eg.Go(func() error {
|
||||
// Do some thing
|
||||
return nil
|
||||
<span class=pl-s1>eg</span>.<span class=pl-en>Go</span>(<span class=pl-k>func</span>() <span class=pl-smi>error</span> {
|
||||
<span class=pl-c>// Do some thing</span>
|
||||
<span class=pl-k>return</span> <span class=pl-c1>nil</span>
|
||||
})
|
||||
|
||||
eg.Go(func() error {
|
||||
// Do other thing
|
||||
return nil
|
||||
<span class=pl-s1>eg</span>.<span class=pl-en>Go</span>(<span class=pl-k>func</span>() <span class=pl-smi>error</span> {
|
||||
<span class=pl-c>// Do other thing</span>
|
||||
<span class=pl-k>return</span> <span class=pl-c1>nil</span>
|
||||
})
|
||||
|
||||
if err := eg.Wait(); err != nil {
|
||||
// Handle error
|
||||
}
|
||||
</code></pre><h3>Use <a href=https://pkg.go.dev/golang.org/x/sync/semaphore>semaphore</a> when need to implement WorkerPool</h3><p>Please don't use external libs for WorkerPool, I don't want to deal with dependency hell.<h2>External libs</h2><h3>No need <code>vendor</code></h3><p>Only need if you need something from <code>vendor</code>, to generate mock or something else.<h3>Use <code>build.go</code> to include build tools in go.mod</h3><p>To easily control version of build tools.<p>For example <code>build.go</code>:<pre><code class=language-go>//go:build tools
|
||||
// +build tools
|
||||
<span class=pl-k>if</span> <span class=pl-s1>err</span> <span class=pl-c1>:=</span> <span class=pl-s1>eg</span>.<span class=pl-en>Wait</span>(); <span class=pl-s1>err</span> <span class=pl-c1>!=</span> <span class=pl-c1>nil</span> {
|
||||
<span class=pl-c>// Handle error</span>
|
||||
}</pre></div><h3><a id=user-content-use-semaphore-when-need-to-implement-workerpool class=anchor aria-hidden=true href=#use-semaphore-when-need-to-implement-workerpool><span aria-hidden=true class="octicon octicon-link"></span></a>Use <a href=https://pkg.go.dev/golang.org/x/sync/semaphore rel=nofollow>semaphore</a> when need to implement WorkerPool</h3><p>Please don't use external libs for WorkerPool, I don't want to deal with dependency hell.<h2><a id=user-content-external-libs class=anchor aria-hidden=true href=#external-libs><span aria-hidden=true class="octicon octicon-link"></span></a>External libs</h2><h3><a id=user-content-no-need-vendor class=anchor aria-hidden=true href=#no-need-vendor><span aria-hidden=true class="octicon octicon-link"></span></a>No need <code>vendor</code></h3><p>Only need if you need something from <code>vendor</code>, to generate mock or something else.<h3><a id=user-content-use-buildgo-to-include-build-tools-in-gomod class=anchor aria-hidden=true href=#use-buildgo-to-include-build-tools-in-gomod><span aria-hidden=true class="octicon octicon-link"></span></a>Use <code>build.go</code> to include build tools in go.mod</h3><p>To easily control version of build tools.<p>For example <code>build.go</code>:<div class="highlight highlight-source-go"><pre><span class=pl-c>//go:build tools</span>
|
||||
<span class=pl-c>// +build tools</span>
|
||||
|
||||
package main
|
||||
<span class=pl-k>package</span> main
|
||||
|
||||
import (
|
||||
_ "github.com/golang/protobuf/protoc-gen-go"
|
||||
)
|
||||
</code></pre><p>And then in <code>Makefile</code>:<pre><code class=language-Makefile>build:
|
||||
go install github.com/golang/protobuf/protoc-gen-go
|
||||
</code></pre><p>We always get the version of build tools in <code>go.mod</code> each time we install it.<br>Future contributors will not cry anymore.<h3>Don't use cli libs (<a href=https://github.com/spf13/cobra>spf13/cobra</a>, <a href=https://github.com/urfave/cli>urfave/cli</a>) just for Go service</h3><p>What is the point to pass many params (<code>do-it</code>, <code>--abc</code>, <code>--xyz</code>) when what we only need is start service?<p>In my case, service starts with only config, and config should be read from file or environment like <a href=https://12factor.net/>The Twelve Factors</a> guide.<h3>Don't use <a href=https://github.com/grpc-ecosystem/grpc-gateway>grpc-ecosystem/grpc-gateway</a></h3><p>Just don't.<p>Use <a href=https://github.com/protocolbuffers/protobuf-go>protocolbuffers/protobuf-go</a>, <a href=https://github.com/grpc/grpc-go>grpc/grpc-go</a> for gRPC.<p>Write 1 for both gRPC, REST sounds good, but in the end, it is not worth it.<h3>Don't use <a href=https://github.com/uber/prototool>uber/prototool</a>, use <a href=https://github.com/bufbuild/buf>bufbuild/buf</a></h3><p>prototool is deprecated, and buf can generate, lint, format as good as prototool.<h3>Use <a href=https://github.com/gin-gonic/gin>gin-gonic/gin</a> for REST.</h3><p>Don't use <code>gin.Context</code> when pass context from handler layer to service layer, use <code>gin.Context.Request.Context()</code> instead.<h3>If you want log, just use <a href=https://github.com/uber-go/zap>uber-go/zap</a></h3><p>It is fast!<ul><li>Don't overuse <code>func (*Logger) With</code>. Because if log line is too long, there is a possibility that we can lost it.<li>Use <code>MarshalLogObject</code> when we need to hide some field of object when log (field is long or has sensitive value)<li>Don't use <code>Panic</code>. Use <code>Fatal</code> for errors when start service to check dependencies. If you really need panic level, use <code>DPanic</code>.<li>If doubt, use <code>zap.Any</code>.<li>Use <code>contextID</code> or <code>traceID</code> in every log lines for easily debug.</ul><h3>To read config, use <a href=https://github.com/spf13/viper>spf13/viper</a></h3><p>Only init config in main or cmd layer.<br>Do not use <code>viper.Get...</code> in business layer or inside business layer.<p>Why?<ul><li>Hard to mock and test<li>Put all config in single place for easily tracking</ul><p>Also, be careful if config value is empty.<br>You should decide to continue or stop the service if there is no config.<h3>Don't overuse ORM libs, no need to handle another layer above SQL.</h3><p>Each ORM libs has each different syntax.<br>To learn and use those libs correctly is time consuming.<br>So just stick to plain SQL.<br>It is easier to debug when something is wrong.<p>But <code>database/sql</code> has its own limit.<br>For example, it is hard to get primary key after insert/update.<br>So may be you want to use ORM for those cases.<br>I hear that <a href=https://github.com/go-gorm/gorm>go-gorm/gorm</a>, <a href=https://github.com/ent/ent>ent/ent</a> is good.<h3>If you want test, just use <a href=https://github.com/stretchr/testify>stretchr/testify</a>.</h3><p>It is easy to write a suite test, thanks to testify.<br>Also, for mocking, there are many options out there.<br>Pick 1 then sleep peacefully.<h3>If need to mock, choose <a href=https://github.com/matryer/moq>matryer/moq</a> or <a href=https://github.com/golang/mock>golang/mock</a></h3><p>The first is easy to use but not powerful as the later.<br>If you want to make sure mock func is called with correct times, use the later.<p>Example with <code>matryer/moq</code>:<pre><code class=language-go>// Only gen mock if source code file is newer than mock file
|
||||
// https://jonwillia.ms/2019/12/22/conditional-gomock-mockgen
|
||||
//go:generate sh -c "test service_mock_generated.go -nt $GOFILE && exit 0; moq -rm -out service_mock_generated.go . Service"
|
||||
</code></pre><h3>Be careful with <a href=https://github.com/spf13/cast>spf13/cast</a></h3><p>Don't cast proto enum:<pre><code class=language-go>// Bad
|
||||
a := cast.ToInt32(servicev1.ReasonCode_ABC)
|
||||
<span class=pl-k>import</span> (
|
||||
_ <span class=pl-s>"github.com/golang/protobuf/protoc-gen-go"</span>
|
||||
)</pre></div><p>And then in <code>Makefile</code>:<div class="highlight highlight-source-makefile"><pre><span class=pl-en>build</span>:
|
||||
go install github.com/golang/protobuf/protoc-gen-go</pre></div><p>We always get the version of build tools in <code>go.mod</code> each time we install it.
|
||||
Future contributors will not cry anymore.<h3><a id=user-content-dont-use-cli-libs-spf13cobra-urfavecli-just-for-go-service class=anchor aria-hidden=true href=#dont-use-cli-libs-spf13cobra-urfavecli-just-for-go-service><span aria-hidden=true class="octicon octicon-link"></span></a>Don't use cli libs (<a href=https://github.com/spf13/cobra>spf13/cobra</a>, <a href=https://github.com/urfave/cli>urfave/cli</a>) just for Go service</h3><p>What is the point to pass many params (<code>do-it</code>, <code>--abc</code>, <code>--xyz</code>) when what we only need is start service?<p>In my case, service starts with only config, and config should be read from file or environment like <a href=https://12factor.net/ rel=nofollow>The Twelve Factors</a> guide.<h3><a id=user-content-dont-use-grpc-ecosystemgrpc-gateway class=anchor aria-hidden=true href=#dont-use-grpc-ecosystemgrpc-gateway><span aria-hidden=true class="octicon octicon-link"></span></a>Don't use <a href=https://github.com/grpc-ecosystem/grpc-gateway>grpc-ecosystem/grpc-gateway</a></h3><p>Just don't.<p>Use <a href=https://github.com/protocolbuffers/protobuf-go>protocolbuffers/protobuf-go</a>, <a href=https://github.com/grpc/grpc-go>grpc/grpc-go</a> for gRPC.<p>Write 1 for both gRPC, REST sounds good, but in the end, it is not worth it.<h3><a id=user-content-dont-use-uberprototool-use-bufbuildbuf class=anchor aria-hidden=true href=#dont-use-uberprototool-use-bufbuildbuf><span aria-hidden=true class="octicon octicon-link"></span></a>Don't use <a href=https://github.com/uber/prototool>uber/prototool</a>, use <a href=https://github.com/bufbuild/buf>bufbuild/buf</a></h3><p>prototool is deprecated, and buf can generate, lint, format as good as prototool.<h3><a id=user-content-use-gin-gonicgin-for-rest class=anchor aria-hidden=true href=#use-gin-gonicgin-for-rest><span aria-hidden=true class="octicon octicon-link"></span></a>Use <a href=https://github.com/gin-gonic/gin>gin-gonic/gin</a> for REST.</h3><p>Don't use <code>gin.Context</code> when pass context from handler layer to service layer, use <code>gin.Context.Request.Context()</code> instead.<h3><a id=user-content-if-you-want-log-just-use-uber-gozap class=anchor aria-hidden=true href=#if-you-want-log-just-use-uber-gozap><span aria-hidden=true class="octicon octicon-link"></span></a>If you want log, just use <a href=https://github.com/uber-go/zap>uber-go/zap</a></h3><p>It is fast!<ul><li>Don't overuse <code>func (*Logger) With</code>. Because if log line is too long, there is a possibility that we can lost it.<li>Use <code>MarshalLogObject</code> when we need to hide some field of object when log (field is long or has sensitive value)<li>Don't use <code>Panic</code>. Use <code>Fatal</code> for errors when start service to check dependencies. If you really need panic level, use <code>DPanic</code>.<li>If doubt, use <code>zap.Any</code>.<li>Use <code>contextID</code> or <code>traceID</code> in every log lines for easily debug.</ul><h3><a id=user-content-to-read-config-use-spf13viper class=anchor aria-hidden=true href=#to-read-config-use-spf13viper><span aria-hidden=true class="octicon octicon-link"></span></a>To read config, use <a href=https://github.com/spf13/viper>spf13/viper</a></h3><p>Only init config in main or cmd layer.
|
||||
Do not use <code>viper.Get...</code> in business layer or inside business layer.<p>Why?<ul><li>Hard to mock and test<li>Put all config in single place for easily tracking</ul><p>Also, be careful if config value is empty.
|
||||
You should decide to continue or stop the service if there is no config.<h3><a id=user-content-dont-overuse-orm-libs-no-need-to-handle-another-layer-above-sql class=anchor aria-hidden=true href=#dont-overuse-orm-libs-no-need-to-handle-another-layer-above-sql><span aria-hidden=true class="octicon octicon-link"></span></a>Don't overuse ORM libs, no need to handle another layer above SQL.</h3><p>Each ORM libs has each different syntax.
|
||||
To learn and use those libs correctly is time consuming.
|
||||
So just stick to plain SQL.
|
||||
It is easier to debug when something is wrong.<p>But <code>database/sql</code> has its own limit.
|
||||
For example, it is hard to get primary key after insert/update.
|
||||
So may be you want to use ORM for those cases.
|
||||
I hear that <a href=https://github.com/go-gorm/gorm>go-gorm/gorm</a>, <a href=https://github.com/ent/ent>ent/ent</a> is good.<h3><a id=user-content-if-you-want-test-just-use-stretchrtestify class=anchor aria-hidden=true href=#if-you-want-test-just-use-stretchrtestify><span aria-hidden=true class="octicon octicon-link"></span></a>If you want test, just use <a href=https://github.com/stretchr/testify>stretchr/testify</a>.</h3><p>It is easy to write a suite test, thanks to testify.
|
||||
Also, for mocking, there are many options out there.
|
||||
Pick 1 then sleep peacefully.<h3><a id=user-content-if-need-to-mock-choose-matryermoq-or-golangmock class=anchor aria-hidden=true href=#if-need-to-mock-choose-matryermoq-or-golangmock><span aria-hidden=true class="octicon octicon-link"></span></a>If need to mock, choose <a href=https://github.com/matryer/moq>matryer/moq</a> or <a href=https://github.com/golang/mock>golang/mock</a></h3><p>The first is easy to use but not powerful as the later.
|
||||
If you want to make sure mock func is called with correct times, use the later.<p>Example with <code>matryer/moq</code>:<div class="highlight highlight-source-go"><pre><span class=pl-c>// Only gen mock if source code file is newer than mock file</span>
|
||||
<span class=pl-c>// https://jonwillia.ms/2019/12/22/conditional-gomock-mockgen</span>
|
||||
<span class=pl-c>//go:generate sh -c "test service_mock_generated.go -nt $GOFILE && exit 0; moq -rm -out service_mock_generated.go . Service"</span></pre></div><h3><a id=user-content-be-careful-with-spf13cast class=anchor aria-hidden=true href=#be-careful-with-spf13cast><span aria-hidden=true class="octicon octicon-link"></span></a>Be careful with <a href=https://github.com/spf13/cast>spf13/cast</a></h3><p>Don't cast proto enum:<div class="highlight highlight-source-go"><pre><span class=pl-c>// Bad</span>
|
||||
<span class=pl-s1>a</span> <span class=pl-c1>:=</span> <span class=pl-s1>cast</span>.<span class=pl-en>ToInt32</span>(<span class=pl-s1>servicev1</span>.<span class=pl-c1>ReasonCode_ABC</span>)
|
||||
|
||||
// Good
|
||||
a := int32(servicev1.ReasonCode_ABC)
|
||||
</code></pre><h3>Use <a href=https://pkg.go.dev/golang.org/x/tools/cmd/stringer>stringer</a> if you want your type enum can be print as string</h3><pre><code class=language-go>type Drink int
|
||||
<span class=pl-c>// Good</span>
|
||||
<span class=pl-s1>a</span> <span class=pl-c1>:=</span> <span class=pl-en>int32</span>(<span class=pl-s1>servicev1</span>.<span class=pl-c1>ReasonCode_ABC</span>)</pre></div><h3><a id=user-content-use-stringer-if-you-want-your-type-enum-can-be-print-as-string class=anchor aria-hidden=true href=#use-stringer-if-you-want-your-type-enum-can-be-print-as-string><span aria-hidden=true class="octicon octicon-link"></span></a>Use <a href=https://pkg.go.dev/golang.org/x/tools/cmd/stringer rel=nofollow>stringer</a> if you want your type enum can be print as string</h3><div class="highlight highlight-source-go"><pre><span class=pl-k>type</span> <span class=pl-smi>Drink</span> <span class=pl-smi>int</span>
|
||||
|
||||
const (
|
||||
Beer Drink = iota
|
||||
Water
|
||||
OrangeJuice
|
||||
)
|
||||
</code></pre><pre><code class=language-sh>go install golang.org/x/tools/cmd/stringer@latest
|
||||
<span class=pl-k>const</span> (
|
||||
<span class=pl-s1>Beer</span> <span class=pl-smi>Drink</span> <span class=pl-c1>=</span> <span class=pl-s1>iota</span>
|
||||
<span class=pl-s1>Water</span>
|
||||
<span class=pl-s1>OrangeJuice</span>
|
||||
)</pre></div><div class="highlight highlight-source-shell"><pre>go install golang.org/x/tools/cmd/stringer@latest
|
||||
|
||||
# Run inside directory which contains Drink
|
||||
stringer -type=Drink
|
||||
</code></pre><h3>Don't waste your time rewrite rate limiter if your use case is simple, use <a href=https://pkg.go.dev/golang.org/x/time/rate>rate</a> or <a href=https://github.com/go-redis/redis_rate>go-redis/redis_rate</a></h3><p>rate if you want rate limiter locally in your single instance of service.<br>redis_rate if you want rate limiter distributed across all your instances of service.<h3>Replace <code>go fmt</code>, <code>goimports</code> with <a href=https://github.com/mvdan/gofumpt>mvdan/gofumpt</a>.</h3><p><code>gofumpt</code> provides more rules when format Go codes.<h3>Use <a href=https://github.com/golangci/golangci-lint>golangci/golangci-lint</a>.</h3><p>No need to say more.<br>Lint or get the f out!<p>If you get <code>fieldalignment</code> error, use <a href=https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/fieldalignment>fieldalignment</a> to fix them.<pre><code class=language-sh># Install
|
||||
<span class=pl-c><span class=pl-c>#</span> Run inside directory which contains Drink</span>
|
||||
stringer -type=Drink</pre></div><h3><a id=user-content-dont-waste-your-time-rewrite-rate-limiter-if-your-use-case-is-simple-use-rate-or-go-redisredis_rate class=anchor aria-hidden=true href=#dont-waste-your-time-rewrite-rate-limiter-if-your-use-case-is-simple-use-rate-or-go-redisredis_rate><span aria-hidden=true class="octicon octicon-link"></span></a>Don't waste your time rewrite rate limiter if your use case is simple, use <a href=https://pkg.go.dev/golang.org/x/time/rate rel=nofollow>rate</a> or <a href=https://github.com/go-redis/redis_rate>go-redis/redis_rate</a></h3><p>rate if you want rate limiter locally in your single instance of service.
|
||||
redis_rate if you want rate limiter distributed across all your instances of service.<h3><a id=user-content-replace-go-fmt-goimports-with-mvdangofumpt class=anchor aria-hidden=true href=#replace-go-fmt-goimports-with-mvdangofumpt><span aria-hidden=true class="octicon octicon-link"></span></a>Replace <code>go fmt</code>, <code>goimports</code> with <a href=https://github.com/mvdan/gofumpt>mvdan/gofumpt</a>.</h3><p><code>gofumpt</code> provides more rules when format Go codes.<h3><a id=user-content-use-golangcigolangci-lint class=anchor aria-hidden=true href=#use-golangcigolangci-lint><span aria-hidden=true class="octicon octicon-link"></span></a>Use <a href=https://github.com/golangci/golangci-lint>golangci/golangci-lint</a>.</h3><p>No need to say more.
|
||||
Lint or get the f out!<p>If you get <code>fieldalignment</code> error, use <a href=https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/fieldalignment rel=nofollow>fieldalignment</a> to fix them.<div class="highlight highlight-source-shell"><pre><span class=pl-c><span class=pl-c>#</span> Install</span>
|
||||
go install golang.org/x/tools/go/analysis/passes/fieldalignment/cmd/fieldalignment@latest
|
||||
|
||||
# Fix
|
||||
fieldalignment -fix ./internal/business/*.go
|
||||
</code></pre><h2>Thanks</h2><ul><li><a href=https://github.com/uber-go/guide/blob/master/style.md>Uber Go Style Guide</a><li><a href=https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis>Functional options for friendly APIs</a><li><a href=https://google.github.io/styleguide/go/index>Google Go Style</a></ul><a href=mailto:hauvipapro+posts@gmail.com>Feel free to ask me via email</a>
|
||||
<span class=pl-c><span class=pl-c>#</span> Fix</span>
|
||||
fieldalignment -fix ./internal/business/<span class=pl-k>*</span>.go</pre></div><h2><a id=user-content-thanks class=anchor aria-hidden=true href=#thanks><span aria-hidden=true class="octicon octicon-link"></span></a>Thanks</h2><ul><li><a href=https://github.com/uber-go/guide/blob/master/style.md>Uber Go Style Guide</a><li><a href=https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis rel=nofollow>Functional options for friendly APIs</a><li><a href=https://google.github.io/styleguide/go/index rel=nofollow>Google Go Style</a></ul><a href=mailto:hauvipapro+posts@gmail.com>Feel free to ask me via email</a>
|
||||
<a rel=me href=https://hachyderm.io/@haunguyen>Mastodon</a>
|
|
@ -1,2 +1,12 @@
|
|||
<!doctype html><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><link rel=preconnect href=https://fonts.googleapis.com><link rel=preconnect href=https://fonts.gstatic.com crossorigin><link href="https://fonts.googleapis.com/css2?family=Recursive:wght,CASL,MONO@300..800,0..1,0..1&display=swap" rel=stylesheet><link href=https://haunt98.github.io/iosevka_webfont/iosevka-term-ss08/iosevka-term-ss08.css rel=stylesheet><link rel=stylesheet href=styles.css><a href=index>Index</a><h1>UUID or else</h1><p>There are many use cases where we need to use a unique ID.<br>In my experience, I only encouter 2 cases:<ul><li>ID to trace request from client to server, from service to service (microservice architecture or nanoservice I don't know).<li>Primary key for database.</ul><p>In my Go universe, there are some libs to help us with this:<ul><li><a href=https://github.com/google/uuid>google/uuid</a><li><a href=https://github.com/rs/xid>rs/xid</a><li><a href=https://github.com/segmentio/ksuid>segmentio/ksuid</a><li><a href=https://github.com/oklog/ulid>oklog/ulid</a></ul><h2>First use case is trace ID, or context aware ID</h2><p>The ID is used only for trace and log.<br>If same ID is generated twice (because maybe the possibilty is too small but not 0), honestly I don't care.<br>When I use that ID to search log , if it pops more than things I care for, it is still no harm to me.<p>My choice for this use case is <strong>rs/xid</strong>.<br>Because it is small (not span too much on log line) and copy friendly.<h2>Second use case is primary key, also hard choice</h2><p>Why I don't use auto increment key for primary key?<br>The answer is simple, I don't want to write database specific SQL.<br>SQLite has some different syntax from MySQL, and PostgreSQL and so on.<br>Every logic I can move to application layer from database layer, I will.<p>In the past and present, I use <strong>google/uuid</strong>, specificially I use UUID v4.<br>In the future I will look to use <strong>segmentio/ksuid</strong> and <strong>oklog/ulid</strong> (trial and error of course).<br>Both are sortable, but <strong>google/uuid</strong> is not.<br>The reason I'm afraid because the database is sensitive subject, and I need more testing and battle test proof to trust those libs.<h2>What else?</h2><p>I think about adding prefix to ID to identify which resource that ID represents.<h2>Thanks</h2><ul><li><a href=https://www.cybertec-postgresql.com/en/uuid-serial-or-identity-columns-for-postgresql-auto-generated-primary-keys/>UUID, SERIAL OR IDENTITY COLUMNS FOR POSTGRESQL AUTO-GENERATED PRIMARY KEYS?</a><li><a href=https://brandur.org/nanoglyphs/026-ids>Identity Crisis: Sequence v. UUID as Primary Key</a><li><a href=https://blog.kowalczyk.info/article/JyRZ/generating-good-unique-ids-in-go.html>Generating good unique ids in Go</a><li><a href=https://encore.dev/blog/go-1.18-generic-identifiers>How we used Go 1.18 when designing our Identifiers</a><li><a href=https://blog.daveallie.com/ulid-primary-keys>ULIDs and Primary Keys</a><li><a href=https://0pointer.net/blog/projects/ids.html>On IDs</a></ul><a href=mailto:hauvipapro+posts@gmail.com>Feel free to ask me via email</a>
|
||||
<!doctype html><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><link rel=stylesheet href=https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.1.0/github-markdown-dark.css><style>.markdown-body{box-sizing:border-box;min-width:200px;max-width:980px;margin:0 auto;padding:45px}@media(max-width:767px){.markdown-body{padding:15px}}</style><body class=markdown-body><a href=index>Index</a><h1><a id=user-content-uuid-or-else class=anchor aria-hidden=true href=#uuid-or-else><span aria-hidden=true class="octicon octicon-link"></span></a>UUID or else</h1><p>There are many use cases where we need to use a unique ID.
|
||||
In my experience, I only encouter 2 cases:<ul><li>ID to trace request from client to server, from service to service (microservice architecture or nanoservice I don't know).<li>Primary key for database.</ul><p>In my Go universe, there are some libs to help us with this:<ul><li><a href=https://github.com/google/uuid>google/uuid</a><li><a href=https://github.com/rs/xid>rs/xid</a><li><a href=https://github.com/segmentio/ksuid>segmentio/ksuid</a><li><a href=https://github.com/oklog/ulid>oklog/ulid</a></ul><h2><a id=user-content-first-use-case-is-trace-id-or-context-aware-id class=anchor aria-hidden=true href=#first-use-case-is-trace-id-or-context-aware-id><span aria-hidden=true class="octicon octicon-link"></span></a>First use case is trace ID, or context aware ID</h2><p>The ID is used only for trace and log.
|
||||
If same ID is generated twice (because maybe the possibilty is too small but not 0), honestly I don't care.
|
||||
When I use that ID to search log , if it pops more than things I care for, it is still no harm to me.<p>My choice for this use case is <strong>rs/xid</strong>.
|
||||
Because it is small (not span too much on log line) and copy friendly.<h2><a id=user-content-second-use-case-is-primary-key-also-hard-choice class=anchor aria-hidden=true href=#second-use-case-is-primary-key-also-hard-choice><span aria-hidden=true class="octicon octicon-link"></span></a>Second use case is primary key, also hard choice</h2><p>Why I don't use auto increment key for primary key?
|
||||
The answer is simple, I don't want to write database specific SQL.
|
||||
SQLite has some different syntax from MySQL, and PostgreSQL and so on.
|
||||
Every logic I can move to application layer from database layer, I will.<p>In the past and present, I use <strong>google/uuid</strong>, specificially I use UUID v4.
|
||||
In the future I will look to use <strong>segmentio/ksuid</strong> and <strong>oklog/ulid</strong> (trial and error of course).
|
||||
Both are sortable, but <strong>google/uuid</strong> is not.
|
||||
The reason I'm afraid because the database is sensitive subject, and I need more testing and battle test proof to trust those libs.<h2><a id=user-content-what-else class=anchor aria-hidden=true href=#what-else><span aria-hidden=true class="octicon octicon-link"></span></a>What else?</h2><p>I think about adding prefix to ID to identify which resource that ID represents.<h2><a id=user-content-thanks class=anchor aria-hidden=true href=#thanks><span aria-hidden=true class="octicon octicon-link"></span></a>Thanks</h2><ul><li><a href=https://www.cybertec-postgresql.com/en/uuid-serial-or-identity-columns-for-postgresql-auto-generated-primary-keys/ rel=nofollow>UUID, SERIAL OR IDENTITY COLUMNS FOR POSTGRESQL AUTO-GENERATED PRIMARY KEYS?</a><li><a href=https://brandur.org/nanoglyphs/026-ids rel=nofollow>Identity Crisis: Sequence v. UUID as Primary Key</a><li><a href=https://blog.kowalczyk.info/article/JyRZ/generating-good-unique-ids-in-go.html rel=nofollow>Generating good unique ids in Go</a><li><a href=https://encore.dev/blog/go-1.18-generic-identifiers rel=nofollow>How we used Go 1.18 when designing our Identifiers</a><li><a href=https://blog.daveallie.com/ulid-primary-keys rel=nofollow>ULIDs and Primary Keys</a><li><a href=https://0pointer.net/blog/projects/ids.html rel=nofollow>On IDs</a></ul><a href=mailto:hauvipapro+posts@gmail.com>Feel free to ask me via email</a>
|
||||
<a rel=me href=https://hachyderm.io/@haunguyen>Mastodon</a>
|
|
@ -1,46 +1,43 @@
|
|||
<!doctype html><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><link rel=preconnect href=https://fonts.googleapis.com><link rel=preconnect href=https://fonts.gstatic.com crossorigin><link href="https://fonts.googleapis.com/css2?family=Recursive:wght,CASL,MONO@300..800,0..1,0..1&display=swap" rel=stylesheet><link href=https://haunt98.github.io/iosevka_webfont/iosevka-term-ss08/iosevka-term-ss08.css rel=stylesheet><link rel=stylesheet href=styles.css><a href=index>Index</a><h1>Migrate to <code>buf</code> from <code>prototool</code></h1><p>Why? Because <code>prototool</code> is outdated, and can not run on M1 mac.<p>We need 3 files:<ul><li><code>build.go</code>: need to install protoc-gen-* binaries with pin version in <code>go.mod</code><li><code>buf.yaml</code><li><code>buf.gen.yaml</code></ul><p>FYI, the libs version I use:<ul><li><a href=https://github.com/golang/protobuf/releases/tag/v1.5.2>golang/protobuf v1.5.2</a><li><a href=https://github.com/grpc-ecosystem/grpc-gateway/releases/tag/v1.16.0>grpc-ecosystem/grpc-gateway v1.16.0</a><li><a href=github.com/envoyproxy/protoc-gen-validate>envoyproxy/protoc-gen-validate</a><li><a href=github.com/kei2100/protoc-gen-marshal-zap>kei2100/protoc-gen-marshal-zap</a></ul><p><code>build.go</code>:<pre><code class=language-go>//go:build tools
|
||||
// +build tools
|
||||
<!doctype html><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><link rel=stylesheet href=https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.1.0/github-markdown-dark.css><style>.markdown-body{box-sizing:border-box;min-width:200px;max-width:980px;margin:0 auto;padding:45px}@media(max-width:767px){.markdown-body{padding:15px}}</style><body class=markdown-body><a href=index>Index</a><h1><a id=user-content-migrate-to-buf-from-prototool class=anchor aria-hidden=true href=#migrate-to-buf-from-prototool><span aria-hidden=true class="octicon octicon-link"></span></a>Migrate to <code>buf</code> from <code>prototool</code></h1><p>Why? Because <code>prototool</code> is outdated, and can not run on M1 mac.<p>We need 3 files:<ul><li><code>build.go</code>: need to install protoc-gen-* binaries with pin version in <code>go.mod</code><li><code>buf.yaml</code><li><code>buf.gen.yaml</code></ul><p>FYI, the libs version I use:<ul><li><a href=https://github.com/golang/protobuf/releases/tag/v1.5.2>golang/protobuf v1.5.2</a><li><a href=https://github.com/grpc-ecosystem/grpc-gateway/releases/tag/v1.16.0>grpc-ecosystem/grpc-gateway v1.16.0</a><li><a href=github.com/envoyproxy/protoc-gen-validate>envoyproxy/protoc-gen-validate</a><li><a href=github.com/kei2100/protoc-gen-marshal-zap>kei2100/protoc-gen-marshal-zap</a></ul><p><code>build.go</code>:<div class="highlight highlight-source-go"><pre><span class=pl-c>//go:build tools</span>
|
||||
<span class=pl-c>// +build tools</span>
|
||||
|
||||
import (
|
||||
_ "github.com/envoyproxy/protoc-gen-validate"
|
||||
_ "github.com/golang/protobuf/protoc-gen-go"
|
||||
_ "github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway"
|
||||
_ "github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger"
|
||||
_ "github.com/kei2100/protoc-gen-marshal-zap/plugin/protoc-gen-marshal-zap"
|
||||
)
|
||||
</code></pre><p><code>buf.yaml</code><pre><code class=language-yaml>version: v1
|
||||
deps:
|
||||
- buf.build/haunt98/googleapis:b38d93f7ade94a698adff9576474ae7c
|
||||
- buf.build/haunt98/grpc-gateway:ecf4f0f58aa8496f8a76ed303c6e06c7
|
||||
- buf.build/haunt98/protoc-gen-validate:2686264610fc4ad4a9fcc932647e279d
|
||||
- buf.build/haunt98/marshal-zap:2a593ca925134680a5820d3f13c1be5a
|
||||
breaking:
|
||||
use:
|
||||
- FILE
|
||||
lint:
|
||||
use:
|
||||
- DEFAULT
|
||||
</code></pre><p><code>buf.gen.yaml</code>:<pre><code class=language-yaml>version: v1
|
||||
plugins:
|
||||
- name: go
|
||||
out: pkg
|
||||
opt:
|
||||
- plugins=grpc
|
||||
- name: grpc-gateway
|
||||
out: pkg
|
||||
opt:
|
||||
- logtostderr=true
|
||||
- name: swagger
|
||||
out: .
|
||||
opt:
|
||||
- logtostderr=true
|
||||
- name: validate
|
||||
out: pkg
|
||||
opt:
|
||||
- lang=go
|
||||
- name: marshal-zap
|
||||
out: pkg
|
||||
</code></pre><p>Update <code>Makefile</code>:<pre><code class=language-Makefile>gen:
|
||||
<span class=pl-k>import</span> (
|
||||
_ <span class=pl-s>"github.com/envoyproxy/protoc-gen-validate"</span>
|
||||
_ <span class=pl-s>"github.com/golang/protobuf/protoc-gen-go"</span>
|
||||
_ <span class=pl-s>"github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway"</span>
|
||||
_ <span class=pl-s>"github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger"</span>
|
||||
_ <span class=pl-s>"github.com/kei2100/protoc-gen-marshal-zap/plugin/protoc-gen-marshal-zap"</span>
|
||||
)</pre></div><p><code>buf.yaml</code><div class="highlight highlight-source-yaml"><pre><span class=pl-ent>version</span>: <span class=pl-c1>v1</span>
|
||||
<span class=pl-ent>deps</span>:
|
||||
- <span class=pl-s>buf.build/haunt98/googleapis:b38d93f7ade94a698adff9576474ae7c</span>
|
||||
- <span class=pl-s>buf.build/haunt98/grpc-gateway:ecf4f0f58aa8496f8a76ed303c6e06c7</span>
|
||||
- <span class=pl-s>buf.build/haunt98/protoc-gen-validate:2686264610fc4ad4a9fcc932647e279d</span>
|
||||
- <span class=pl-s>buf.build/haunt98/marshal-zap:2a593ca925134680a5820d3f13c1be5a</span>
|
||||
<span class=pl-ent>breaking</span>:
|
||||
<span class=pl-ent>use</span>:
|
||||
- <span class=pl-s>FILE</span>
|
||||
<span class=pl-ent>lint</span>:
|
||||
<span class=pl-ent>use</span>:
|
||||
- <span class=pl-s>DEFAULT</span></pre></div><p><code>buf.gen.yaml</code>:<div class="highlight highlight-source-yaml"><pre><span class=pl-ent>version</span>: <span class=pl-c1>v1</span>
|
||||
<span class=pl-ent>plugins</span>:
|
||||
- <span class=pl-ent>name</span>: <span class=pl-s>go</span>
|
||||
<span class=pl-ent>out</span>: <span class=pl-s>pkg</span>
|
||||
<span class=pl-ent>opt</span>:
|
||||
- <span class=pl-s>plugins=grpc</span>
|
||||
- <span class=pl-ent>name</span>: <span class=pl-s>grpc-gateway</span>
|
||||
<span class=pl-ent>out</span>: <span class=pl-s>pkg</span>
|
||||
<span class=pl-ent>opt</span>:
|
||||
- <span class=pl-s>logtostderr=true</span>
|
||||
- <span class=pl-ent>name</span>: <span class=pl-s>swagger</span>
|
||||
<span class=pl-ent>out</span>: <span class=pl-s>.</span>
|
||||
<span class=pl-ent>opt</span>:
|
||||
- <span class=pl-s>logtostderr=true</span>
|
||||
- <span class=pl-ent>name</span>: <span class=pl-s>validate</span>
|
||||
<span class=pl-ent>out</span>: <span class=pl-s>pkg</span>
|
||||
<span class=pl-ent>opt</span>:
|
||||
- <span class=pl-s>lang=go</span>
|
||||
- <span class=pl-ent>name</span>: <span class=pl-s>marshal-zap</span>
|
||||
<span class=pl-ent>out</span>: <span class=pl-s>pkg</span></pre></div><p>Update <code>Makefile</code>:<div class="highlight highlight-source-makefile"><pre><span class=pl-en>gen</span>:
|
||||
go install github.com/golang/protobuf/protoc-gen-go
|
||||
go install github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway
|
||||
go install github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger
|
||||
|
@ -49,6 +46,5 @@ plugins:
|
|||
go install github.com/bufbuild/buf/cmd/buf@latest
|
||||
buf mod update
|
||||
buf format -w
|
||||
buf generate
|
||||
</code></pre><p>Run <code>make gen</code> to have fun of course.<h2>FAQ</h2><p>Remember <code>grpc-ecosystem/grpc-gateway</code>, <code>envoyproxy/protoc-gen-validate</code>, <code>kei2100/protoc-gen-marshal-zap</code> is optional, so feel free to delete if you don't use theme.<p>If use <code>vendor</code>:<ul><li>Replace <code>buf generate</code> with <code>buf generate --exclude-path vendor</code>.<li>Replace <code>buf format -w</code> with <code>buf format -w --exclude-path vendor</code>.</ul><p>If you use grpc-gateway:<ul><li>Replace <code>import "third_party/googleapis/google/api/annotations.proto";</code> with <code>import "google/api/annotations.proto";</code><li>Delete <code>security_definitions</code>, <code>security</code>, in <code>option (grpc.gateway.protoc_gen_swagger.options.openapiv2_swagger)</code>.</ul><p>The last step is delete <code>prototool.yaml</code>.<p>If you are not migrate but start from scratch:<ul><li>Add <code>buf lint</code> to make sure your proto is good.<li>Add <code>buf breaking --against "https://your-grpc-repo-goes-here.git"</code> to make sure each time you update proto, you don't break backward compatibility.</ul><h2>Thanks</h2><ul><li><a href=https://github.com/uber/prototool>uber/prototool</a><li><a href=https://github.com/bufbuild/buf>bufbuild/buf</a></ul><a href=mailto:hauvipapro+posts@gmail.com>Feel free to ask me via email</a>
|
||||
buf generate</pre></div><p>Run <code>make gen</code> to have fun of course.<h2><a id=user-content-faq class=anchor aria-hidden=true href=#faq><span aria-hidden=true class="octicon octicon-link"></span></a>FAQ</h2><p>Remember <code>grpc-ecosystem/grpc-gateway</code>, <code>envoyproxy/protoc-gen-validate</code>, <code>kei2100/protoc-gen-marshal-zap</code> is optional, so feel free to delete if you don't use theme.<p>If use <code>vendor</code>:<ul><li>Replace <code>buf generate</code> with <code>buf generate --exclude-path vendor</code>.<li>Replace <code>buf format -w</code> with <code>buf format -w --exclude-path vendor</code>.</ul><p>If you use grpc-gateway:<ul><li>Replace <code>import "third_party/googleapis/google/api/annotations.proto";</code> with <code>import "google/api/annotations.proto";</code><li>Delete <code>security_definitions</code>, <code>security</code>, in <code>option (grpc.gateway.protoc_gen_swagger.options.openapiv2_swagger)</code>.</ul><p>The last step is delete <code>prototool.yaml</code>.<p>If you are not migrate but start from scratch:<ul><li>Add <code>buf lint</code> to make sure your proto is good.<li>Add <code>buf breaking --against "https://your-grpc-repo-goes-here.git"</code> to make sure each time you update proto, you don't break backward compatibility.</ul><h2><a id=user-content-thanks class=anchor aria-hidden=true href=#thanks><span aria-hidden=true class="octicon octicon-link"></span></a>Thanks</h2><ul><li><a href=https://github.com/uber/prototool>uber/prototool</a><li><a href=https://github.com/bufbuild/buf>bufbuild/buf</a></ul><a href=mailto:hauvipapro+posts@gmail.com>Feel free to ask me via email</a>
|
||||
<a rel=me href=https://hachyderm.io/@haunguyen>Mastodon</a>
|
|
@ -1,30 +1,34 @@
|
|||
<!doctype html><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><link rel=preconnect href=https://fonts.googleapis.com><link rel=preconnect href=https://fonts.gstatic.com crossorigin><link href="https://fonts.googleapis.com/css2?family=Recursive:wght,CASL,MONO@300..800,0..1,0..1&display=swap" rel=stylesheet><link href=https://haunt98.github.io/iosevka_webfont/iosevka-term-ss08/iosevka-term-ss08.css rel=stylesheet><link rel=stylesheet href=styles.css><a href=index>Index</a><h1>Experiment Go</h1><p>There come a time when you need to experiment new things, new style, new approach.<br>So this post serves as it is named.<h1>Design API by trimming down the interface/struct or whatever</h1><p>Instead of:<pre><code class=language-go>type Client interface {
|
||||
GetUser()
|
||||
AddUser()
|
||||
GetAccount()
|
||||
RemoveAccount()
|
||||
<!doctype html><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><link rel=stylesheet href=https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.1.0/github-markdown-dark.css><style>.markdown-body{box-sizing:border-box;min-width:200px;max-width:980px;margin:0 auto;padding:45px}@media(max-width:767px){.markdown-body{padding:15px}}</style><body class=markdown-body><a href=index>Index</a><h1><a id=user-content-experiment-go class=anchor aria-hidden=true href=#experiment-go><span aria-hidden=true class="octicon octicon-link"></span></a>Experiment Go</h1><p>There come a time when you need to experiment new things, new style, new approach.
|
||||
So this post serves as it is named.<h1><a id=user-content-design-api-by-trimming-down-the-interfacestruct-or-whatever class=anchor aria-hidden=true href=#design-api-by-trimming-down-the-interfacestruct-or-whatever><span aria-hidden=true class="octicon octicon-link"></span></a>Design API by trimming down the interface/struct or whatever</h1><p>Instead of:<div class="highlight highlight-source-go"><pre><span class=pl-k>type</span> <span class=pl-smi>Client</span> <span class=pl-k>interface</span> {
|
||||
<span class=pl-c1>GetUser</span>()
|
||||
<span class=pl-c1>AddUser</span>()
|
||||
<span class=pl-c1>GetAccount</span>()
|
||||
<span class=pl-c1>RemoveAccount</span>()
|
||||
}
|
||||
|
||||
// c is Client
|
||||
c.GetUser()
|
||||
c.RemoveAccount()
|
||||
</code></pre><p>Try:<pre><code class=language-go>type Client struct {
|
||||
User ClientUser
|
||||
Account ClientAccount
|
||||
<span class=pl-c>// c is Client</span>
|
||||
<span class=pl-s1>c</span>.<span class=pl-en>GetUser</span>()
|
||||
<span class=pl-s1>c</span>.<span class=pl-en>RemoveAccount</span>()</pre></div><p>Try:<div class="highlight highlight-source-go"><pre><span class=pl-k>type</span> <span class=pl-smi>Client</span> <span class=pl-k>struct</span> {
|
||||
<span class=pl-c1>User</span> <span class=pl-smi>ClientUser</span>
|
||||
<span class=pl-c1>Account</span> <span class=pl-smi>ClientAccount</span>
|
||||
}
|
||||
|
||||
type ClientUser interface {
|
||||
Get()
|
||||
Add()
|
||||
<span class=pl-k>type</span> <span class=pl-smi>ClientUser</span> <span class=pl-k>interface</span> {
|
||||
<span class=pl-c1>Get</span>()
|
||||
<span class=pl-c1>Add</span>()
|
||||
}
|
||||
|
||||
type ClientAccount interface {
|
||||
Get()
|
||||
Remove()
|
||||
<span class=pl-k>type</span> <span class=pl-smi>ClientAccount</span> <span class=pl-k>interface</span> {
|
||||
<span class=pl-c1>Get</span>()
|
||||
<span class=pl-c1>Remove</span>()
|
||||
}
|
||||
|
||||
// c is Client
|
||||
c.User.Get()
|
||||
c.Account.Remove()
|
||||
</code></pre><p>The difference is <code>c.GetUser()</code> -> <code>c.User.Get()</code>.<p>For example we have client which connect to bank.<br>There are many functions like <code>GetUser</code>, <code>GetTransaction</code>, <code>VerifyAccount</code>, ...<br>So split big client to many children, each child handle single aspect, like user or transaction.<p>My concert is we replace an interface with a struct which contains multiple interfaces aka children.<br>I don't know if this is the right call.<p>This pattern is used by <a href=https://github.com/google/go-github>google/go-github</a>.<h2>Find alternative to <a href=https://github.com/grpc/grpc-go>grpc/grpc-go</a></h2><p>Why?<br><a href="https://github.com/grpc/grpc-go/issues?q=is%3Aissue+compatibility+is%3Aclosed">See for yourself</a>.<br>Also read <a href=https://go.dev/blog/protobuf-apiv2>A new Go API for Protocol Buffers</a> to know why <code>v1.20.0</code> is <code>v2</code>.<p>Currently there are some:<ul><li><a href=https://github.com/bufbuild/connect-go>bufbuild/connect-go</a>. Comming from buf, trust worthy but need time to make it match feature parity with grpc-go.<li><a href=https://github.com/twitchtv/twirp>twitchtv/twirp</a><li><a href=https://github.com/storj/drpc>storj/drpc</a></ul><h1>Thanks</h1><ul><li><a href=https://blog.gopheracademy.com/advent-2019/api-clients-humans/>API Clients for Humans</a></ul><a href=mailto:hauvipapro+posts@gmail.com>Feel free to ask me via email</a>
|
||||
<span class=pl-c>// c is Client</span>
|
||||
<span class=pl-s1>c</span>.<span class=pl-c1>User</span>.<span class=pl-en>Get</span>()
|
||||
<span class=pl-s1>c</span>.<span class=pl-c1>Account</span>.<span class=pl-en>Remove</span>()</pre></div><p>The difference is <code>c.GetUser()</code> -> <code>c.User.Get()</code>.<p>For example we have client which connect to bank.
|
||||
There are many functions like <code>GetUser</code>, <code>GetTransaction</code>, <code>VerifyAccount</code>, ...
|
||||
So split big client to many children, each child handle single aspect, like user or transaction.<p>My concert is we replace an interface with a struct which contains multiple interfaces aka children.
|
||||
I don't know if this is the right call.<p>This pattern is used by <a href=https://github.com/google/go-github>google/go-github</a>.<h2><a id=user-content-find-alternative-to-grpcgrpc-go class=anchor aria-hidden=true href=#find-alternative-to-grpcgrpc-go><span aria-hidden=true class="octicon octicon-link"></span></a>Find alternative to <a href=https://github.com/grpc/grpc-go>grpc/grpc-go</a></h2><p>Why?
|
||||
<a href="https://github.com/grpc/grpc-go/issues?q=is%3Aissue+compatibility+is%3Aclosed">See for yourself</a>.
|
||||
Also read <a href=https://go.dev/blog/protobuf-apiv2 rel=nofollow>A new Go API for Protocol Buffers</a> to know why <code>v1.20.0</code> is <code>v2</code>.<p>Currently there are some:<ul><li><a href=https://github.com/bufbuild/connect-go>bufbuild/connect-go</a>. Comming from buf, trust worthy but need time to make it match feature parity with grpc-go.<li><a href=https://github.com/twitchtv/twirp>twitchtv/twirp</a><li><a href=https://github.com/storj/drpc>storj/drpc</a></ul><h1><a id=user-content-thanks class=anchor aria-hidden=true href=#thanks><span aria-hidden=true class="octicon octicon-link"></span></a>Thanks</h1><ul><li><a href=https://blog.gopheracademy.com/advent-2019/api-clients-humans/ rel=nofollow>API Clients for Humans</a></ul><a href=mailto:hauvipapro+posts@gmail.com>Feel free to ask me via email</a>
|
||||
<a rel=me href=https://hachyderm.io/@haunguyen>Mastodon</a>
|
|
@ -1,16 +1,27 @@
|
|||
<!doctype html><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><link rel=preconnect href=https://fonts.googleapis.com><link rel=preconnect href=https://fonts.gstatic.com crossorigin><link href="https://fonts.googleapis.com/css2?family=Recursive:wght,CASL,MONO@300..800,0..1,0..1&display=swap" rel=stylesheet><link href=https://haunt98.github.io/iosevka_webfont/iosevka-term-ss08/iosevka-term-ss08.css rel=stylesheet><link rel=stylesheet href=styles.css><a href=index>Index</a><h1>SQL</h1><p>Previously in my fresher software developer time, I rarely write SQL, I always use ORM to wrap SQL.<br>But time past and too much abstraction bites me.<br>So I decide to only write SQL from now as much as possible, no more ORM for me.<br>But if there is any cool ORM for Go, I guess I try.<p>This guide is not kind of guide which cover all cases.<br>Just my little tricks when I work with SQL.<h2>Stay away from database unique id</h2><p>Use UUID instead.<br>If you can, and you should, choose UUID type which can be sortable.<h2>Stay away from database timestamp</h2><p>Stay away from all kind of database timestamp (MySQL timestmap, SQLite timestamp, ...)<br>Just use int64 then pass the timestamp in service layer not database layer.<p>Why? Because time and date and location are too much complex to handle.<br>In my business, I use timestamp in milliseconds.<br>Then I save timestamp as int64 value to database.<br>Each time I get timestamp from database, I parse to time struct in Go with location or format I want.<br>No more hassle!<p>It looks like this:<pre><code class=language-txt>[Business] time, data -> convert to unix timestamp milliseconds -> [Database] int64
|
||||
</code></pre><h2>Use index!!!</h2><p>You should use index for faster query, but not too much.<br>Don't create index for every fields in table.<br>Choose wisely!<p>For example, create index in MySQL:<pre><code class=language-sql>CREATE INDEX `idx_timestamp`
|
||||
ON `user_upload` (`timestamp`);
|
||||
</code></pre><h2>Be careful with NULL</h2><p>If compare with field which can be NULL, remember to check NULL for safety.<pre><code class=language-sql>-- field_something can be NULL
|
||||
<!doctype html><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><link rel=stylesheet href=https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.1.0/github-markdown-dark.css><style>.markdown-body{box-sizing:border-box;min-width:200px;max-width:980px;margin:0 auto;padding:45px}@media(max-width:767px){.markdown-body{padding:15px}}</style><body class=markdown-body><a href=index>Index</a><h1><a id=user-content-sql class=anchor aria-hidden=true href=#sql><span aria-hidden=true class="octicon octicon-link"></span></a>SQL</h1><p>Previously in my fresher software developer time, I rarely write SQL, I always use ORM to wrap SQL.
|
||||
But time past and too much abstraction bites me.
|
||||
So I decide to only write SQL from now as much as possible, no more ORM for me.
|
||||
But if there is any cool ORM for Go, I guess I try.<p>This guide is not kind of guide which cover all cases.
|
||||
Just my little tricks when I work with SQL.<h2><a id=user-content-stay-away-from-database-unique-id class=anchor aria-hidden=true href=#stay-away-from-database-unique-id><span aria-hidden=true class="octicon octicon-link"></span></a>Stay away from database unique id</h2><p>Use UUID instead.
|
||||
If you can, and you should, choose UUID type which can be sortable.<h2><a id=user-content-stay-away-from-database-timestamp class=anchor aria-hidden=true href=#stay-away-from-database-timestamp><span aria-hidden=true class="octicon octicon-link"></span></a>Stay away from database timestamp</h2><p>Stay away from all kind of database timestamp (MySQL timestmap, SQLite timestamp, ...)
|
||||
Just use int64 then pass the timestamp in service layer not database layer.<p>Why? Because time and date and location are too much complex to handle.
|
||||
In my business, I use timestamp in milliseconds.
|
||||
Then I save timestamp as int64 value to database.
|
||||
Each time I get timestamp from database, I parse to time struct in Go with location or format I want.
|
||||
No more hassle!<p>It looks like this:<div class="highlight highlight-text-adblock"><pre>[Business] time, data -> convert to unix timestamp milliseconds -> [Database] int64</pre></div><h2><a id=user-content-use-index class=anchor aria-hidden=true href=#use-index><span aria-hidden=true class="octicon octicon-link"></span></a>Use index!!!</h2><p>You should use index for faster query, but not too much.
|
||||
Don't create index for every fields in table.
|
||||
Choose wisely!<p>For example, create index in MySQL:<div class="highlight highlight-source-sql"><pre><span class=pl-k>CREATE</span> <span class=pl-k>INDEX</span> `<span class=pl-en>idx_timestamp</span>`
|
||||
<span class=pl-k>ON</span> <span class=pl-s><span class=pl-pds>`</span>user_upload<span class=pl-pds>`</span></span> (<span class=pl-s><span class=pl-pds>`</span>timestamp<span class=pl-pds>`</span></span>);</pre></div><h2><a id=user-content-be-careful-with-null class=anchor aria-hidden=true href=#be-careful-with-null><span aria-hidden=true class="octicon octicon-link"></span></a>Be careful with NULL</h2><p>If compare with field which can be NULL, remember to check NULL for safety.<div class="highlight highlight-source-sql"><pre><span class=pl-c><span class=pl-c>--</span> field_something can be NULL</span>
|
||||
|
||||
-- Bad
|
||||
SELECT *
|
||||
FROM table
|
||||
WHERE field_something != 1
|
||||
<span class=pl-c><span class=pl-c>--</span> Bad</span>
|
||||
<span class=pl-k>SELECT</span> <span class=pl-k>*</span>
|
||||
<span class=pl-k>FROM</span> table
|
||||
<span class=pl-k>WHERE</span> field_something <span class=pl-k>!=</span> <span class=pl-c1>1</span>
|
||||
|
||||
-- Good
|
||||
SELECT *
|
||||
FROM table
|
||||
WHERE (field_something IS NULL OR field_something != 1)
|
||||
</code></pre><p>Need clarify why this happpen? Idk :(<h2><code>VARCHAR</code> or <code>TEXT</code></h2><p>Prefer <code>VARCHAR</code> if you need to query and of course use index, and make sure size of value will never hit the limit.<br>Prefer <code>TEXT</code> if you don't care, just want to store something.<h2>Be super careful when migrate, update database on production and online!!!</h2><p>Plase read docs about online ddl operations before do anything online (keep database running the same time update it, for example create index, ...)<ul><li><a href=https://dev.mysql.com/doc/refman/5.7/en/innodb-online-ddl-operations.html>For MySQL 5.7</a>, <a href=https://dev.mysql.com/doc/refman/5.7/en/innodb-online-ddl-limitations.html>Limitations</a><li><a href=https://dev.mysql.com/doc/refman/8.0/en/innodb-online-ddl-operations.html>For MySQL 8.0</a>, <a href=https://dev.mysql.com/doc/refman/8.0/en/innodb-online-ddl-limitations.html>Limitations</a></ul><h2>Tools</h2><ul><li>Use <a href=https://github.com/sqlfluff/sqlfluff>sqlfluff/sqlfluff</a> to check your SQL.<li>Use <a href=https://github.com/k1LoW/tbls>k1LoW/tbls</a> to grasp your database reality :)</ul><h2>Thanks</h2><ul><li><a href=https://use-the-index-luke.com/>Use The Index, Luke</a><li><a href=https://www.foxhound.systems/blog/essential-elements-of-high-performance-sql-indexes/>Essential elements of high performance applications: SQL indexes</a><li><a href=https://architecturenotes.co/things-you-should-know-about-databases/>Things You Should Know About Databases</a></ul><a href=mailto:hauvipapro+posts@gmail.com>Feel free to ask me via email</a>
|
||||
<span class=pl-c><span class=pl-c>--</span> Good</span>
|
||||
<span class=pl-k>SELECT</span> <span class=pl-k>*</span>
|
||||
<span class=pl-k>FROM</span> table
|
||||
<span class=pl-k>WHERE</span> (field_something IS <span class=pl-k>NULL</span> <span class=pl-k>OR</span> field_something <span class=pl-k>!=</span> <span class=pl-c1>1</span>)</pre></div><p>Need clarify why this happpen? Idk :(<h2><a id=user-content-varchar-or-text class=anchor aria-hidden=true href=#varchar-or-text><span aria-hidden=true class="octicon octicon-link"></span></a>
|
||||
<code>VARCHAR</code> or <code>TEXT</code></h2><p>Prefer <code>VARCHAR</code> if you need to query and of course use index, and make sure size of value will never hit the limit.
|
||||
Prefer <code>TEXT</code> if you don't care, just want to store something.<h2><a id=user-content-be-super-careful-when-migrate-update-database-on-production-and-online class=anchor aria-hidden=true href=#be-super-careful-when-migrate-update-database-on-production-and-online><span aria-hidden=true class="octicon octicon-link"></span></a>Be super careful when migrate, update database on production and online!!!</h2><p>Plase read docs about online ddl operations before do anything online (keep database running the same time update it, for example create index, ...)<ul><li><a href=https://dev.mysql.com/doc/refman/5.7/en/innodb-online-ddl-operations.html rel=nofollow>For MySQL 5.7</a>, <a href=https://dev.mysql.com/doc/refman/5.7/en/innodb-online-ddl-limitations.html rel=nofollow>Limitations</a><li><a href=https://dev.mysql.com/doc/refman/8.0/en/innodb-online-ddl-operations.html rel=nofollow>For MySQL 8.0</a>, <a href=https://dev.mysql.com/doc/refman/8.0/en/innodb-online-ddl-limitations.html rel=nofollow>Limitations</a></ul><h2><a id=user-content-tools class=anchor aria-hidden=true href=#tools><span aria-hidden=true class="octicon octicon-link"></span></a>Tools</h2><ul><li>Use <a href=https://github.com/sqlfluff/sqlfluff>sqlfluff/sqlfluff</a> to check your SQL.<li>Use <a href=https://github.com/k1LoW/tbls>k1LoW/tbls</a> to grasp your database reality :)</ul><h2><a id=user-content-thanks class=anchor aria-hidden=true href=#thanks><span aria-hidden=true class="octicon octicon-link"></span></a>Thanks</h2><ul><li><a href=https://use-the-index-luke.com/ rel=nofollow>Use The Index, Luke</a><li><a href=https://www.foxhound.systems/blog/essential-elements-of-high-performance-sql-indexes/ rel=nofollow>Essential elements of high performance applications: SQL indexes</a><li><a href=https://architecturenotes.co/things-you-should-know-about-databases/ rel=nofollow>Things You Should Know About Databases</a></ul><a href=mailto:hauvipapro+posts@gmail.com>Feel free to ask me via email</a>
|
||||
<a rel=me href=https://hachyderm.io/@haunguyen>Mastodon</a>
|
|
@ -1,20 +1,17 @@
|
|||
<!doctype html><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><link rel=preconnect href=https://fonts.googleapis.com><link rel=preconnect href=https://fonts.gstatic.com crossorigin><link href="https://fonts.googleapis.com/css2?family=Recursive:wght,CASL,MONO@300..800,0..1,0..1&display=swap" rel=stylesheet><link href=https://haunt98.github.io/iosevka_webfont/iosevka-term-ss08/iosevka-term-ss08.css rel=stylesheet><link rel=stylesheet href=styles.css><a href=index>Index</a><h1>gitignore</h1><p>My quick check for <code>.gitignore</code>.<h2>Base</h2><pre><code class=language-txt># macOS
|
||||
<!doctype html><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><link rel=stylesheet href=https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.1.0/github-markdown-dark.css><style>.markdown-body{box-sizing:border-box;min-width:200px;max-width:980px;margin:0 auto;padding:45px}@media(max-width:767px){.markdown-body{padding:15px}}</style><body class=markdown-body><a href=index>Index</a><h1><a id=user-content-gitignore class=anchor aria-hidden=true href=#gitignore><span aria-hidden=true class="octicon octicon-link"></span></a>gitignore</h1><p>My quick check for <code>.gitignore</code>.<h2><a id=user-content-base class=anchor aria-hidden=true href=#base><span aria-hidden=true class="octicon octicon-link"></span></a>Base</h2><div class="highlight highlight-text-adblock"><pre><span class=pl-c># macOS</span>
|
||||
.DS_Store
|
||||
|
||||
# Windows
|
||||
*.exe
|
||||
<span class=pl-c># Windows</span>
|
||||
<span class=pl-k>*</span>.exe
|
||||
|
||||
# IntelliJ
|
||||
<span class=pl-c># IntelliJ</span>
|
||||
.idea/
|
||||
|
||||
# VSCode
|
||||
.vscode/
|
||||
</code></pre><h2>Go</h2><pre><code class=language-txt># Go
|
||||
# Test coverage
|
||||
<span class=pl-c># VSCode</span>
|
||||
.vscode/</pre></div><h2><a id=user-content-go class=anchor aria-hidden=true href=#go><span aria-hidden=true class="octicon octicon-link"></span></a>Go</h2><div class="highlight highlight-text-adblock"><pre><span class=pl-c># Go</span>
|
||||
<span class=pl-c># Test coverage</span>
|
||||
coverage.out
|
||||
|
||||
# Should ignore vendor
|
||||
vendor
|
||||
</code></pre><h2>Python</h2><pre><code class=language-txt>venv
|
||||
</code></pre><a href=mailto:hauvipapro+posts@gmail.com>Feel free to ask me via email</a>
|
||||
<span class=pl-c># Should ignore vendor</span>
|
||||
vendor</pre></div><h2><a id=user-content-python class=anchor aria-hidden=true href=#python><span aria-hidden=true class="octicon octicon-link"></span></a>Python</h2><div class="highlight highlight-text-adblock"><pre>venv</pre></div><a href=mailto:hauvipapro+posts@gmail.com>Feel free to ask me via email</a>
|
||||
<a rel=me href=https://hachyderm.io/@haunguyen>Mastodon</a>
|
|
@ -1,46 +1,47 @@
|
|||
<!doctype html><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><link rel=preconnect href=https://fonts.googleapis.com><link rel=preconnect href=https://fonts.gstatic.com crossorigin><link href="https://fonts.googleapis.com/css2?family=Recursive:wght,CASL,MONO@300..800,0..1,0..1&display=swap" rel=stylesheet><link href=https://haunt98.github.io/iosevka_webfont/iosevka-term-ss08/iosevka-term-ss08.css rel=stylesheet><link rel=stylesheet href=styles.css><a href=index>Index</a><h1>Reload config</h1><p>This serves as design draft of reload config system<pre><code class=language-plantuml>@startuml Reload config
|
||||
<!doctype html><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><link rel=stylesheet href=https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.1.0/github-markdown-dark.css><style>.markdown-body{box-sizing:border-box;min-width:200px;max-width:980px;margin:0 auto;padding:45px}@media(max-width:767px){.markdown-body{padding:15px}}</style><body class=markdown-body><a href=index>Index</a><h1><a id=user-content-reload-config class=anchor aria-hidden=true href=#reload-config><span aria-hidden=true class="octicon octicon-link"></span></a>Reload config</h1><p>This serves as design draft of reload config system<div class="highlight highlight-source-wsd"><pre><span class=pl-k>@startuml</span> Reload config
|
||||
|
||||
skinparam defaultFontName Iosevka Term SS08
|
||||
<span class=pl-k>skinparam</span> <span class=pl-k>defaultFontName</span> <span class=pl-s>Iosevka Term SS08</span>
|
||||
|
||||
participant admin
|
||||
participant other_service
|
||||
participant config_service
|
||||
participant storage
|
||||
<span class=pl-k>participant</span> <span class=pl-c1>admin</span>
|
||||
<span class=pl-k>participant</span> <span class=pl-c1>other_service</span>
|
||||
<span class=pl-k>participant</span> <span class=pl-c1>config_service</span>
|
||||
<span class=pl-k>participant</span> <span class=pl-c1>storage</span>
|
||||
|
||||
== Admin handle ==
|
||||
<span class=pl-k>==</span> <span class=pl-s>Admin handle</span> <span class=pl-k>==</span>
|
||||
|
||||
admin -> config_service: set/update/delete config
|
||||
<span class=pl-c1>admin</span> <span class=pl-k>-></span> <span class=pl-c1>config_service</span>: set/update/delete config
|
||||
|
||||
config_service -> storage: set/update/delete config
|
||||
<span class=pl-c1>config_service</span> <span class=pl-k>-></span> <span class=pl-c1>storage</span>: set/update/delete config
|
||||
|
||||
== Other service handle ==
|
||||
<span class=pl-k>==</span> <span class=pl-s>Other service handle</span> <span class=pl-k>==</span>
|
||||
|
||||
other_service -> other_service: init service
|
||||
<span class=pl-c1>other_service</span> <span class=pl-k>-></span> <span class=pl-c1>other_service</span>: init service
|
||||
|
||||
activate other_service
|
||||
<span class=pl-k>activate</span> <span class=pl-c1>other_service</span>
|
||||
|
||||
other_service -> storage: make connection
|
||||
<span class=pl-c1>other_service</span> <span class=pl-k>-></span> <span class=pl-c1>storage</span>: make connection
|
||||
|
||||
loop
|
||||
<span class=pl-k>loop</span>
|
||||
|
||||
other_service -> storage: listen on config change
|
||||
<span class=pl-c1>other_service</span> <span class=pl-k>-></span> <span class=pl-c1>storage</span>: listen on config change
|
||||
|
||||
other_service -> other_service: save config to memory
|
||||
<span class=pl-c1>other_service</span> <span class=pl-k>-></span> <span class=pl-c1>other_service</span>: save config to memory
|
||||
|
||||
end
|
||||
<span class=pl-k>end</span>
|
||||
|
||||
deactivate other_service
|
||||
<span class=pl-k>deactivate</span> <span class=pl-c1>other_service</span>
|
||||
|
||||
other_service -> other_service: do business
|
||||
<span class=pl-c1>other_service</span> <span class=pl-k>-></span> <span class=pl-c1>other_service</span>: do business
|
||||
|
||||
activate other_service
|
||||
<span class=pl-k>activate</span> <span class=pl-c1>other_service</span>
|
||||
|
||||
other_service -> other_service: get config
|
||||
<span class=pl-c1>other_service</span> <span class=pl-k>-></span> <span class=pl-c1>other_service</span>: get config
|
||||
|
||||
other_service -> other_service: do other business
|
||||
<span class=pl-c1>other_service</span> <span class=pl-k>-></span> <span class=pl-c1>other_service</span>: do other business
|
||||
|
||||
deactivate other_service
|
||||
<span class=pl-k>deactivate</span> <span class=pl-c1>other_service</span>
|
||||
|
||||
@enduml
|
||||
</code></pre><p>Config storage can be any key value storage or database like etcd, Consul, mySQL, ...<p>If storage is key value storage, maybe there is API to listen on config change.<br>Otherwise we should create a loop to get all config from storage for some interval, for example each 5 minute.<p>Each <code>other_service</code> need to get config from its memory, not hit <code>storage</code>.<br>So there is some delay between upstream config (config in <code>storage</code>) and downstream config (config in <code>other_service</code>), but maybe we can forgive that delay (???).<p>Pros:<ul><li><p>Config can be dynamic, service does not need to restart to apply new config.<li><p>Each service only keep 1 connection to <code>storage</code> to listen to config change, not hit <code>storage</code> for each request.</ul><p>Cons:<ul><li>Each service has 1 more dependency, aka <code>storage</code>.<li>Need to handle fallback config, incase <code>storage</code> failure.<li>Delay between upstream/downstream config</ul><a href=mailto:hauvipapro+posts@gmail.com>Feel free to ask me via email</a>
|
||||
<span class=pl-k>@enduml</span></pre></div><p>Config storage can be any key value storage or database like etcd, Consul, mySQL, ...<p>If storage is key value storage, maybe there is API to listen on config change.
|
||||
Otherwise we should create a loop to get all config from storage for some interval, for example each 5 minute.<p>Each <code>other_service</code> need to get config from its memory, not hit <code>storage</code>.
|
||||
So there is some delay between upstream config (config in <code>storage</code>) and downstream config (config in <code>other_service</code>), but maybe we can forgive that delay (???).<p>Pros:<ul><li><p>Config can be dynamic, service does not need to restart to apply new config.<li><p>Each service only keep 1 connection to <code>storage</code> to listen to config change, not hit <code>storage</code> for each request.</ul><p>Cons:<ul><li>Each service has 1 more dependency, aka <code>storage</code>.<li>Need to handle fallback config, incase <code>storage</code> failure.<li>Delay between upstream/downstream config</ul><a href=mailto:hauvipapro+posts@gmail.com>Feel free to ask me via email</a>
|
||||
<a rel=me href=https://hachyderm.io/@haunguyen>Mastodon</a>
|
|
@ -1,126 +1,102 @@
|
|||
<!doctype html><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><link rel=preconnect href=https://fonts.googleapis.com><link rel=preconnect href=https://fonts.gstatic.com crossorigin><link href="https://fonts.googleapis.com/css2?family=Recursive:wght,CASL,MONO@300..800,0..1,0..1&display=swap" rel=stylesheet><link href=https://haunt98.github.io/iosevka_webfont/iosevka-term-ss08/iosevka-term-ss08.css rel=stylesheet><link rel=stylesheet href=styles.css><a href=index>Index</a><h1>Install Arch Linux</h1><p>Install Arch Linux is thing I always want to do for my laptop/PC since I had my laptop in ninth grade.<p>This is not a guide for everyone, this is just save for myself in a future and for anyone who want to walk in my shoes.<h2><a href=https://wiki.archlinux.org/index.php/Installation_guide>Installation guide</a></h2><h3>Pre-installation</h3><p>Check disks carefully:<pre><code class=language-sh>lsblk
|
||||
</code></pre><p><a href=https://wiki.archlinux.org/index.php/USB_flash_installation_medium>USB flash installation medium</a><h4>Verify the boot mode</h4><p>Check UEFI mode:<pre><code class=language-sh>ls /sys/firmware/efi/efivars
|
||||
</code></pre><h4>Connect to the internet</h4><p>For wifi, use <a href=https://wiki.archlinux.org/index.php/Iwd>iwd</a>.<h4>Partition the disks</h4><p><a href=https://wiki.archlinux.org/index.php/GPT_fdisk>GPT fdisk</a>:<pre><code class=language-sh>cgdisk /dev/sdx
|
||||
</code></pre><p><a href=https://wiki.archlinux.org/index.php/Partitioning#Partition_scheme>Partition scheme</a><p>UEFI/GPT layout:<table><thead><tr><th>Mount point<th>Partition<th>Partition type<th>Suggested size<tbody><tr><td><code>/mnt/efi</code><td><code>/dev/efi_system_partition</code><td>EFI System Partition<td>512 MiB<tr><td><code>/mnt/boot</code><td><code>/dev/extended_boot_loader_partition</code><td>Extended Boot Loader Partition<td>1 GiB<tr><td><code>/mnt</code><td><code>/dev/root_partition</code><td>Root Partition<td></table><p>BIOS/GPT layout:<table><thead><tr><th>Mount point<th>Partition<th>Partition type<th>Suggested size<tbody><tr><td><td><td>BIOS boot partition<td>1 MiB<tr><td><code>/mnt</code><td><code>/dev/root_partition</code><td>Root Partition<td></table><p>LVM:<pre><code class=language-sh># Create physical volumes
|
||||
<!doctype html><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><link rel=stylesheet href=https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.1.0/github-markdown-dark.css><style>.markdown-body{box-sizing:border-box;min-width:200px;max-width:980px;margin:0 auto;padding:45px}@media(max-width:767px){.markdown-body{padding:15px}}</style><body class=markdown-body><a href=index>Index</a><h1><a id=user-content-install-arch-linux class=anchor aria-hidden=true href=#install-arch-linux><span aria-hidden=true class="octicon octicon-link"></span></a>Install Arch Linux</h1><p>Install Arch Linux is thing I always want to do for my laptop/PC since I had my laptop in ninth grade.<p>This is not a guide for everyone, this is just save for myself in a future and for anyone who want to walk in my shoes.<h2><a id=user-content-installation-guide class=anchor aria-hidden=true href=#installation-guide><span aria-hidden=true class="octicon octicon-link"></span></a><a href=https://wiki.archlinux.org/index.php/Installation_guide rel=nofollow>Installation guide</a></h2><h3><a id=user-content-pre-installation class=anchor aria-hidden=true href=#pre-installation><span aria-hidden=true class="octicon octicon-link"></span></a>Pre-installation</h3><p>Check disks carefully:<div class="highlight highlight-source-shell"><pre>lsblk</pre></div><p><a href=https://wiki.archlinux.org/index.php/USB_flash_installation_medium rel=nofollow>USB flash installation medium</a><h4><a id=user-content-verify-the-boot-mode class=anchor aria-hidden=true href=#verify-the-boot-mode><span aria-hidden=true class="octicon octicon-link"></span></a>Verify the boot mode</h4><p>Check UEFI mode:<div class="highlight highlight-source-shell"><pre>ls /sys/firmware/efi/efivars</pre></div><h4><a id=user-content-connect-to-the-internet class=anchor aria-hidden=true href=#connect-to-the-internet><span aria-hidden=true class="octicon octicon-link"></span></a>Connect to the internet</h4><p>For wifi, use <a href=https://wiki.archlinux.org/index.php/Iwd rel=nofollow>iwd</a>.<h4><a id=user-content-partition-the-disks class=anchor aria-hidden=true href=#partition-the-disks><span aria-hidden=true class="octicon octicon-link"></span></a>Partition the disks</h4><p><a href=https://wiki.archlinux.org/index.php/GPT_fdisk rel=nofollow>GPT fdisk</a>:<div class="highlight highlight-source-shell"><pre>cgdisk /dev/sdx</pre></div><p><a href=https://wiki.archlinux.org/index.php/Partitioning#Partition_scheme rel=nofollow>Partition scheme</a><p>UEFI/GPT layout:<table><thead><tr><th>Mount point<th>Partition<th>Partition type<th>Suggested size<tbody><tr><td><code>/mnt/efi</code><td><code>/dev/efi_system_partition</code><td>EFI System Partition<td>512 MiB<tr><td><code>/mnt/boot</code><td><code>/dev/extended_boot_loader_partition</code><td>Extended Boot Loader Partition<td>1 GiB<tr><td><code>/mnt</code><td><code>/dev/root_partition</code><td>Root Partition<td></table><p>BIOS/GPT layout:<table><thead><tr><th>Mount point<th>Partition<th>Partition type<th>Suggested size<tbody><tr><td><td><td>BIOS boot partition<td>1 MiB<tr><td><code>/mnt</code><td><code>/dev/root_partition</code><td>Root Partition<td></table><p>LVM:<div class="highlight highlight-source-shell"><pre><span class=pl-c><span class=pl-c>#</span> Create physical volumes</span>
|
||||
pvcreate /dev/sdaX
|
||||
|
||||
# Create volume groups
|
||||
<span class=pl-c><span class=pl-c>#</span> Create volume groups</span>
|
||||
vgcreate RootGroup /dev/sdaX /dev/sdaY
|
||||
|
||||
# Create logical volumes
|
||||
lvcreate -l +100%FREE RootGroup -n rootvol
|
||||
</code></pre><p>Format:<pre><code class=language-sh># efi
|
||||
<span class=pl-c><span class=pl-c>#</span> Create logical volumes</span>
|
||||
lvcreate -l +100%FREE RootGroup -n rootvol</pre></div><p>Format:<div class="highlight highlight-source-shell"><pre><span class=pl-c><span class=pl-c>#</span> efi</span>
|
||||
mkfs.fat -F32 /dev/efi_system_partition
|
||||
|
||||
# boot
|
||||
<span class=pl-c><span class=pl-c>#</span> boot</span>
|
||||
mkfs.fat -F32 /dev/extended_boot_loader_partition
|
||||
|
||||
# root
|
||||
<span class=pl-c><span class=pl-c>#</span> root</span>
|
||||
mkfs.ext4 -L ROOT /dev/root_partition
|
||||
|
||||
# root with btrfs
|
||||
<span class=pl-c><span class=pl-c>#</span> root with btrfs</span>
|
||||
mkfs.btrfs -L ROOT /dev/root_partition
|
||||
|
||||
# root on lvm
|
||||
mkfs.ext4 /dev/RootGroup/rootvol
|
||||
</code></pre><p>Mount:<pre><code class=language-sh># root
|
||||
<span class=pl-c><span class=pl-c>#</span> root on lvm</span>
|
||||
mkfs.ext4 /dev/RootGroup/rootvol</pre></div><p>Mount:<div class="highlight highlight-source-shell"><pre><span class=pl-c><span class=pl-c>#</span> root</span>
|
||||
mount /dev/root_partition /mnt
|
||||
|
||||
# root with btrfs
|
||||
<span class=pl-c><span class=pl-c>#</span> root with btrfs</span>
|
||||
mount -o compress=zstd /dev/root_partition /mnt
|
||||
|
||||
# root on lvm
|
||||
<span class=pl-c><span class=pl-c>#</span> root on lvm</span>
|
||||
mount /dev/RootGroup/rootvol /mnt
|
||||
|
||||
# efi
|
||||
<span class=pl-c><span class=pl-c>#</span> efi</span>
|
||||
mount --mkdir /dev/efi_system_partition /mnt/efi
|
||||
|
||||
# boot
|
||||
mount --mkdir /dev/extended_boot_loader_partition /mnt/boot
|
||||
</code></pre><h3>Installation</h3><pre><code class=language-sh>pacstrap -K /mnt base linux linux-firmware
|
||||
<span class=pl-c><span class=pl-c>#</span> boot</span>
|
||||
mount --mkdir /dev/extended_boot_loader_partition /mnt/boot</pre></div><h3><a id=user-content-installation class=anchor aria-hidden=true href=#installation><span aria-hidden=true class="octicon octicon-link"></span></a>Installation</h3><div class="highlight highlight-source-shell"><pre>pacstrap -K /mnt base linux linux-firmware
|
||||
|
||||
# AMD
|
||||
<span class=pl-c><span class=pl-c>#</span> AMD</span>
|
||||
pacstrap -K /mnt amd-ucode
|
||||
|
||||
# Intel
|
||||
<span class=pl-c><span class=pl-c>#</span> Intel</span>
|
||||
pacstrap -K /mnt intel-ucode
|
||||
|
||||
# Btrfs
|
||||
<span class=pl-c><span class=pl-c>#</span> Btrfs</span>
|
||||
pacstrap -K /mnt btrfs-progs
|
||||
|
||||
# LVM
|
||||
<span class=pl-c><span class=pl-c>#</span> LVM</span>
|
||||
pacstrap -K /mnt lvm2
|
||||
|
||||
# Text editor
|
||||
pacstrap -K /mnt neovim
|
||||
</code></pre><h3>Configure</h3><h4><a href=https://wiki.archlinux.org/index.php/Fstab>fstab</a></h4><pre><code class=language-sh>genfstab -U /mnt >> /mnt/etc/fstab
|
||||
</code></pre><h4>Chroot</h4><pre><code class=language-sh>arch-chroot /mnt
|
||||
</code></pre><h4>Time zone</h4><pre><code class=language-sh>ln -sf /usr/share/zoneinfo/Region/City /etc/localtime
|
||||
<span class=pl-c><span class=pl-c>#</span> Text editor</span>
|
||||
pacstrap -K /mnt neovim</pre></div><h3><a id=user-content-configure class=anchor aria-hidden=true href=#configure><span aria-hidden=true class="octicon octicon-link"></span></a>Configure</h3><h4><a id=user-content-fstab class=anchor aria-hidden=true href=#fstab><span aria-hidden=true class="octicon octicon-link"></span></a><a href=https://wiki.archlinux.org/index.php/Fstab rel=nofollow>fstab</a></h4><div class="highlight highlight-source-shell"><pre>genfstab -U /mnt <span class=pl-k>>></span> /mnt/etc/fstab</pre></div><h4><a id=user-content-chroot class=anchor aria-hidden=true href=#chroot><span aria-hidden=true class="octicon octicon-link"></span></a>Chroot</h4><div class="highlight highlight-source-shell"><pre>arch-chroot /mnt</pre></div><h4><a id=user-content-time-zone class=anchor aria-hidden=true href=#time-zone><span aria-hidden=true class="octicon octicon-link"></span></a>Time zone</h4><div class="highlight highlight-source-shell"><pre>ln -sf /usr/share/zoneinfo/Region/City /etc/localtime
|
||||
|
||||
hwclock --systohc
|
||||
</code></pre><h4>Localization:</h4><p>Edit <code>/etc/locale.gen</code>:<pre><code class=language-txt># Uncomment en_US.UTF-8 UTF-8
|
||||
</code></pre><p>Generate locales:<pre><code class=language-sh>locale-gen
|
||||
</code></pre><p>Edit <code>/etc/locale.conf</code>:<pre><code class=language-txt>LANG=en_US.UTF-8
|
||||
</code></pre><h4>Network configuration</h4><p>Edit <code>/etc/hostname</code>:<pre><code class=language-txt>myhostname
|
||||
</code></pre><h4>Initramfs</h4><p>Edit <code>/etc/mkinitcpio.conf</code>:<pre><code class=language-txt># LVM
|
||||
# https://wiki.archlinux.org/title/Install_Arch_Linux_on_LVM#Adding_mkinitcpio_hooks
|
||||
hwclock --systohc</pre></div><h4><a id=user-content-localization class=anchor aria-hidden=true href=#localization><span aria-hidden=true class="octicon octicon-link"></span></a>Localization:</h4><p>Edit <code>/etc/locale.gen</code>:<div class="highlight highlight-text-adblock"><pre><span class=pl-c># Uncomment en_US.UTF-8 UTF-8</span></pre></div><p>Generate locales:<div class="highlight highlight-source-shell"><pre>locale-gen</pre></div><p>Edit <code>/etc/locale.conf</code>:<div class="highlight highlight-text-adblock"><pre>LANG=en_US.UTF-8</pre></div><h4><a id=user-content-network-configuration class=anchor aria-hidden=true href=#network-configuration><span aria-hidden=true class="octicon octicon-link"></span></a>Network configuration</h4><p>Edit <code>/etc/hostname</code>:<div class="highlight highlight-text-adblock"><pre>myhostname</pre></div><h4><a id=user-content-initramfs class=anchor aria-hidden=true href=#initramfs><span aria-hidden=true class="octicon octicon-link"></span></a>Initramfs</h4><p>Edit <code>/etc/mkinitcpio.conf</code>:<div class="highlight highlight-text-adblock"><pre><span class=pl-c># LVM</span>
|
||||
<span class=pl-c># https://wiki.archlinux.org/title/Install_Arch_Linux_on_LVM#Adding_mkinitcpio_hooks</span>
|
||||
HOOKS=(base udev ... block lvm2 filesystems)
|
||||
|
||||
# https://wiki.archlinux.org/title/mkinitcpio#Common_hooks
|
||||
# Replace udev with systemd
|
||||
</code></pre><pre><code class=language-sh>mkinitcpio -P
|
||||
</code></pre><h4>Root password</h4><pre><code class=language-sh>passwd
|
||||
</code></pre><h4>Addition</h4><pre><code class=language-sh># NetworkManager
|
||||
<span class=pl-c># https://wiki.archlinux.org/title/mkinitcpio#Common_hooks</span>
|
||||
<span class=pl-c># Replace udev with systemd</span></pre></div><div class="highlight highlight-source-shell"><pre>mkinitcpio -P</pre></div><h4><a id=user-content-root-password class=anchor aria-hidden=true href=#root-password><span aria-hidden=true class="octicon octicon-link"></span></a>Root password</h4><div class="highlight highlight-source-shell"><pre>passwd</pre></div><h4><a id=user-content-addition class=anchor aria-hidden=true href=#addition><span aria-hidden=true class="octicon octicon-link"></span></a>Addition</h4><div class="highlight highlight-source-shell"><pre><span class=pl-c><span class=pl-c>#</span> NetworkManager</span>
|
||||
pacman -Syu networkmanager
|
||||
systemctl enable NetworkManager.service
|
||||
systemctl <span class=pl-c1>enable</span> NetworkManager.service
|
||||
|
||||
# Bluetooth
|
||||
<span class=pl-c><span class=pl-c>#</span> Bluetooth</span>
|
||||
pacman -Syu bluez
|
||||
systemctl enable bluetooth.service
|
||||
systemctl <span class=pl-c1>enable</span> bluetooth.service
|
||||
|
||||
# Clock
|
||||
timedatectl set-ntp true
|
||||
</code></pre><h4>Boot loader</h4><p><a href=Applications/System/systemd-boot.md>systemd-boot</a><p><a href=https://wiki.archlinux.org/index.php/GRUB>GRUB</a><h2><a href=https://wiki.archlinux.org/index.php/General_recommendations>General recommendations</a></h2><p>Always remember to check <strong>dependencies</strong> when install packages.<h3>System administration</h3><p><a href=https://wiki.archlinux.org/index.php/sudo>Sudo</a>:<pre><code class=language-sh>pacman -Syu sudo
|
||||
<span class=pl-c><span class=pl-c>#</span> Clock</span>
|
||||
timedatectl set-ntp <span class=pl-c1>true</span></pre></div><h4><a id=user-content-boot-loader class=anchor aria-hidden=true href=#boot-loader><span aria-hidden=true class="octicon octicon-link"></span></a>Boot loader</h4><p><a href=Applications/System/systemd-boot.md>systemd-boot</a><p><a href=https://wiki.archlinux.org/index.php/GRUB rel=nofollow>GRUB</a><h2><a id=user-content-general-recommendations class=anchor aria-hidden=true href=#general-recommendations><span aria-hidden=true class="octicon octicon-link"></span></a><a href=https://wiki.archlinux.org/index.php/General_recommendations rel=nofollow>General recommendations</a></h2><p>Always remember to check <strong>dependencies</strong> when install packages.<h3><a id=user-content-system-administration class=anchor aria-hidden=true href=#system-administration><span aria-hidden=true class="octicon octicon-link"></span></a>System administration</h3><p><a href=https://wiki.archlinux.org/index.php/sudo rel=nofollow>Sudo</a>:<div class="highlight highlight-source-shell"><pre>pacman -Syu sudo
|
||||
|
||||
EDITOR=nvim visudo
|
||||
# Uncomment group wheel
|
||||
<span class=pl-c><span class=pl-c>#</span> Uncomment group wheel</span>
|
||||
|
||||
# Add user if don't want to use systemd-homed
|
||||
useradd -m -G wheel -c "The Joker" joker
|
||||
<span class=pl-c><span class=pl-c>#</span> Add user if don't want to use systemd-homed</span>
|
||||
useradd -m -G wheel -c <span class=pl-s><span class=pl-pds>"</span>The Joker<span class=pl-pds>"</span></span> joker
|
||||
|
||||
# Or using zsh
|
||||
useradd -m -G wheel -s /usr/bin/zsh -c "The Joker" joker
|
||||
<span class=pl-c><span class=pl-c>#</span> Or using zsh</span>
|
||||
useradd -m -G wheel -s /usr/bin/zsh -c <span class=pl-s><span class=pl-pds>"</span>The Joker<span class=pl-pds>"</span></span> joker
|
||||
|
||||
# Set password
|
||||
passwd joker
|
||||
</code></pre><p><a href=https://wiki.archlinux.org/index.php/Systemd-homed>systemd-homed (WIP)</a>:<pre><code class=language-sh>systemctl enable systemd-homed.service
|
||||
<span class=pl-c><span class=pl-c>#</span> Set password</span>
|
||||
passwd joker</pre></div><p><a href=https://wiki.archlinux.org/index.php/Systemd-homed rel=nofollow>systemd-homed (WIP)</a>:<div class="highlight highlight-source-shell"><pre>systemctl <span class=pl-c1>enable</span> systemd-homed.service
|
||||
|
||||
homectl create joker --real-name="The Joker" --member-of=wheel
|
||||
homectl create joker --real-name=<span class=pl-s><span class=pl-pds>"</span>The Joker<span class=pl-pds>"</span></span> --member-of=wheel
|
||||
|
||||
# Using zsh
|
||||
homectl update joker --shell=/usr/bin/zsh
|
||||
</code></pre><p><strong>Note</strong>:<br>Can not run <code>homectl</code> when install Arch Linux.<br>Should run on the first boot.<h3>Desktop Environment</h3><p>Install <a href=https://wiki.archlinux.org/index.php/Xorg>Xorg</a>:<pre><code class=language-sh>pacman -Syu xorg-server
|
||||
</code></pre><h4><a href=https://wiki.archlinux.org/index.php/GNOME>GNOME</a></h4><pre><code class=language-sh>pacman -Syu gnome-shell \
|
||||
<span class=pl-c><span class=pl-c>#</span> Using zsh</span>
|
||||
homectl update joker --shell=/usr/bin/zsh</pre></div><p><strong>Note</strong>:
|
||||
Can not run <code>homectl</code> when install Arch Linux.
|
||||
Should run on the first boot.<h3><a id=user-content-desktop-environment class=anchor aria-hidden=true href=#desktop-environment><span aria-hidden=true class="octicon octicon-link"></span></a>Desktop Environment</h3><p>Install <a href=https://wiki.archlinux.org/index.php/Xorg rel=nofollow>Xorg</a>:<div class="highlight highlight-source-shell"><pre>pacman -Syu xorg-server</pre></div><h4><a id=user-content-gnome class=anchor aria-hidden=true href=#gnome><span aria-hidden=true class="octicon octicon-link"></span></a><a href=https://wiki.archlinux.org/index.php/GNOME rel=nofollow>GNOME</a></h4><div class="highlight highlight-source-shell"><pre>pacman -Syu gnome-shell \
|
||||
gnome-control-center gnome-system-monitor \
|
||||
gnome-tweaks gnome-backgrounds gnome-screenshot gnome-keyring gnome-logs \
|
||||
gnome-console gnome-text-editor \
|
||||
nautilus xdg-user-dirs-gtk file-roller evince eog
|
||||
|
||||
# Login manager
|
||||
<span class=pl-c><span class=pl-c>#</span> Login manager</span>
|
||||
pacman -Syu gdm
|
||||
systemctl enable gdm.service
|
||||
</code></pre><h4><a href=https://wiki.archlinux.org/title/KDE>KDE (WIP)</a></h4><pre><code class=language-sh>pacman -Syu plasma-meta \
|
||||
systemctl <span class=pl-c1>enable</span> gdm.service</pre></div><h4><a id=user-content-kde-wip class=anchor aria-hidden=true href=#kde-wip><span aria-hidden=true class="octicon octicon-link"></span></a><a href=https://wiki.archlinux.org/title/KDE rel=nofollow>KDE (WIP)</a></h4><div class="highlight highlight-source-shell"><pre>pacman -Syu plasma-meta \
|
||||
kde-system-meta
|
||||
|
||||
# Login manager
|
||||
<span class=pl-c><span class=pl-c>#</span> Login manager</span>
|
||||
pacman -Syu sddm
|
||||
systemctl enable sddm.service
|
||||
</code></pre><h2><a href=https://wiki.archlinux.org/index.php/List_of_applications>List of applications</a></h2><h3><a href=https://wiki.archlinux.org/index.php/pacman>pacman</a></h3><p>Uncomment in <code>/etc/pacman.conf</code>:<pre><code class=language-txt># Misc options
|
||||
systemctl <span class=pl-c1>enable</span> sddm.service</pre></div><h2><a id=user-content-list-of-applications class=anchor aria-hidden=true href=#list-of-applications><span aria-hidden=true class="octicon octicon-link"></span></a><a href=https://wiki.archlinux.org/index.php/List_of_applications rel=nofollow>List of applications</a></h2><h3><a id=user-content-pacman class=anchor aria-hidden=true href=#pacman><span aria-hidden=true class="octicon octicon-link"></span></a><a href=https://wiki.archlinux.org/index.php/pacman rel=nofollow>pacman</a></h3><p>Uncomment in <code>/etc/pacman.conf</code>:<div class="highlight highlight-text-adblock"><pre><span class=pl-c># Misc options</span>
|
||||
Color
|
||||
ParallelDownloads
|
||||
</code></pre><h3><a href=https://wiki.archlinux.org/title/PipeWire>Pipewire (WIP)</a></h3><pre><code class=language-sh>pacman -Syu pipewire wireplumber \
|
||||
ParallelDownloads</pre></div><h3><a id=user-content-pipewire-wip class=anchor aria-hidden=true href=#pipewire-wip><span aria-hidden=true class="octicon octicon-link"></span></a><a href=https://wiki.archlinux.org/title/PipeWire rel=nofollow>Pipewire (WIP)</a></h3><div class="highlight highlight-source-shell"><pre>pacman -Syu pipewire wireplumber \
|
||||
pipewire-alsa pipewire-pulse \
|
||||
gst-plugin-pipewire pipewire-v4l2
|
||||
</code></pre><h3><a href=https://wiki.archlinux.org/title/Flatpak>Flatpak (WIP)</a></h3><pre><code class=language-sh>pacman -Syu flatpak
|
||||
</code></pre><h2><a href=https://wiki.archlinux.org/index.php/improving_performance>Improving performance</a></h2><p><a href=https://wiki.archlinux.org/index.php/swap#Swap_file>https://wiki.archlinux.org/index.php/swap#Swap_file</a><p><a href=https://wiki.archlinux.org/index.php/swap#Swappiness>https://wiki.archlinux.org/index.php/swap#Swappiness</a><p><a href=https://wiki.archlinux.org/index.php/Systemd/Journal#Journal_size_limit>https://wiki.archlinux.org/index.php/Systemd/Journal#Journal_size_limit</a><p><a href=https://wiki.archlinux.org/index.php/Core_dump#Disabling_automatic_core_dumps>https://wiki.archlinux.org/index.php/Core_dump#Disabling_automatic_core_dumps</a><p><a href=https://wiki.archlinux.org/index.php/Solid_state_drive#Periodic_TRIM>https://wiki.archlinux.org/index.php/Solid_state_drive#Periodic_TRIM</a><p><a href=https://wiki.archlinux.org/index.php/Silent_boot>https://wiki.archlinux.org/index.php/Silent_boot</a><p><a href=https://wiki.archlinux.org/title/Improving_performance#Watchdogs>https://wiki.archlinux.org/title/Improving_performance#Watchdogs</a><p><a href=https://wiki.archlinux.org/title/PRIME>https://wiki.archlinux.org/title/PRIME</a><h2>In the end</h2><p>This guide is updated regularly I promise.</p><a href=mailto:hauvipapro+posts@gmail.com>Feel free to ask me via email</a>
|
||||
gst-plugin-pipewire pipewire-v4l2</pre></div><h3><a id=user-content-flatpak-wip class=anchor aria-hidden=true href=#flatpak-wip><span aria-hidden=true class="octicon octicon-link"></span></a><a href=https://wiki.archlinux.org/title/Flatpak rel=nofollow>Flatpak (WIP)</a></h3><div class="highlight highlight-source-shell"><pre>pacman -Syu flatpak</pre></div><h2><a id=user-content-improving-performance class=anchor aria-hidden=true href=#improving-performance><span aria-hidden=true class="octicon octicon-link"></span></a><a href=https://wiki.archlinux.org/index.php/improving_performance rel=nofollow>Improving performance</a></h2><p><a href=https://wiki.archlinux.org/index.php/swap#Swap_file rel=nofollow>https://wiki.archlinux.org/index.php/swap#Swap_file</a><p><a href=https://wiki.archlinux.org/index.php/swap#Swappiness rel=nofollow>https://wiki.archlinux.org/index.php/swap#Swappiness</a><p><a href=https://wiki.archlinux.org/index.php/Systemd/Journal#Journal_size_limit rel=nofollow>https://wiki.archlinux.org/index.php/Systemd/Journal#Journal_size_limit</a><p><a href=https://wiki.archlinux.org/index.php/Core_dump#Disabling_automatic_core_dumps rel=nofollow>https://wiki.archlinux.org/index.php/Core_dump#Disabling_automatic_core_dumps</a><p><a href=https://wiki.archlinux.org/index.php/Solid_state_drive#Periodic_TRIM rel=nofollow>https://wiki.archlinux.org/index.php/Solid_state_drive#Periodic_TRIM</a><p><a href=https://wiki.archlinux.org/index.php/Silent_boot rel=nofollow>https://wiki.archlinux.org/index.php/Silent_boot</a><p><a href=https://wiki.archlinux.org/title/Improving_performance#Watchdogs rel=nofollow>https://wiki.archlinux.org/title/Improving_performance#Watchdogs</a><p><a href=https://wiki.archlinux.org/title/PRIME rel=nofollow>https://wiki.archlinux.org/title/PRIME</a><h2><a id=user-content-in-the-end class=anchor aria-hidden=true href=#in-the-end><span aria-hidden=true class="octicon octicon-link"></span></a>In the end</h2><p>This guide is updated regularly I promise.</p><a href=mailto:hauvipapro+posts@gmail.com>Feel free to ask me via email</a>
|
||||
<a rel=me href=https://hachyderm.io/@haunguyen>Mastodon</a>
|
|
@ -1,2 +1,4 @@
|
|||
<!doctype html><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><link rel=preconnect href=https://fonts.googleapis.com><link rel=preconnect href=https://fonts.gstatic.com crossorigin><link href="https://fonts.googleapis.com/css2?family=Recursive:wght,CASL,MONO@300..800,0..1,0..1&display=swap" rel=stylesheet><link href=https://haunt98.github.io/iosevka_webfont/iosevka-term-ss08/iosevka-term-ss08.css rel=stylesheet><link rel=stylesheet href=styles.css><a href=index>Index</a><h1>Integration Go gRPC with Buf</h1><p>There are 2 questions here.<br>What is Buf?<br>And why is Buf?</p><a href=mailto:hauvipapro+posts@gmail.com>Feel free to ask me via email</a>
|
||||
<!doctype html><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><link rel=stylesheet href=https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.1.0/github-markdown-dark.css><style>.markdown-body{box-sizing:border-box;min-width:200px;max-width:980px;margin:0 auto;padding:45px}@media(max-width:767px){.markdown-body{padding:15px}}</style><body class=markdown-body><a href=index>Index</a><h1><a id=user-content-integration-go-grpc-with-buf class=anchor aria-hidden=true href=#integration-go-grpc-with-buf><span aria-hidden=true class="octicon octicon-link"></span></a>Integration Go gRPC with Buf</h1><p>There are 2 questions here.
|
||||
What is Buf?
|
||||
And why is Buf?</p><a href=mailto:hauvipapro+posts@gmail.com>Feel free to ask me via email</a>
|
||||
<a rel=me href=https://hachyderm.io/@haunguyen>Mastodon</a>
|
|
@ -1,182 +1,203 @@
|
|||
<!doctype html><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><link rel=preconnect href=https://fonts.googleapis.com><link rel=preconnect href=https://fonts.gstatic.com crossorigin><link href="https://fonts.googleapis.com/css2?family=Recursive:wght,CASL,MONO@300..800,0..1,0..1&display=swap" rel=stylesheet><link href=https://haunt98.github.io/iosevka_webfont/iosevka-term-ss08/iosevka-term-ss08.css rel=stylesheet><link rel=stylesheet href=styles.css><a href=index>Index</a><h1>Speed up writing Go test ASAP</h1><p>Imagine your project currently have 0% unit test code coverage.<br>And your boss keep pushing it to 80% or even 90%?<br>What do you do?<br>Give up?<p>What if I tell you there is a way?<br>Not entirely cheating but ... you know, there is always trade off.<p>If your purpose is to test carefully all path, check if all return is correctly.<br>Sadly this post is not for you, I guess.<br>If you only want good number on test coverage, with minimum effort as possible, I hope this will show you some idea you can use :)<p>In my opinion, unit test is not that important (like must must have).<br>It's just make sure your code is running excatly as you intent it to be.<br>If you don't think about edge case before, unit test won't help you.<h2>First, rewrite the impossible (to test) out</h2><p>When I learn programming, I encounter very interesting idea, which become mainly my mindset when I dev later.<br>I don't recall it clearly, kinda like: "Don't just fix bugs, rewrite it so that kind of bugs will not appear again".<br>So in our context, there is some thing we hardly or can not write test in Go.<br>My suggestion is don't use that thing.<p>In my experience, I can list a few here:<ul><li>Read config each time call func (<code>viper.Get...</code>). You can and you should init all config when project starts.<li>Not use Dependency Injection (DI). There are too many posts in Internet tell you how to do DI properly.<li>Use global var (Except global var <code>Err...</code>). You should move all global var to fields inside some struct.</ul><h2>Let the fun (writing test) begin</h2><p>If you code Go long enough, you know table driven tests and how is that so useful.<br>You set up test data, then you test.<br>Somewhere in the future, you change the func, then you need to update test data, then you good!<p>In simple case, your func only have 2 or 3 inputs so table drive tests is still looking good.<br>But real world is ugly (maybe not, idk I'm just too young in this industry). Your func can have 5 or 10 inputs, also your func call many third party services.<p>Imagine having below func to upload image:<pre><code class=language-go>type service struct {
|
||||
db DB
|
||||
redis Redis
|
||||
minio MinIO
|
||||
logService LogService
|
||||
verifyService VerifyService
|
||||
<!doctype html><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><link rel=stylesheet href=https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.1.0/github-markdown-dark.css><style>.markdown-body{box-sizing:border-box;min-width:200px;max-width:980px;margin:0 auto;padding:45px}@media(max-width:767px){.markdown-body{padding:15px}}</style><body class=markdown-body><a href=index>Index</a><h1><a id=user-content-speed-up-writing-go-test-asap class=anchor aria-hidden=true href=#speed-up-writing-go-test-asap><span aria-hidden=true class="octicon octicon-link"></span></a>Speed up writing Go test ASAP</h1><p>Imagine your project currently have 0% unit test code coverage.
|
||||
And your boss keep pushing it to 80% or even 90%?
|
||||
What do you do?
|
||||
Give up?<p>What if I tell you there is a way?
|
||||
Not entirely cheating but ... you know, there is always trade off.<p>If your purpose is to test carefully all path, check if all return is correctly.
|
||||
Sadly this post is not for you, I guess.
|
||||
If you only want good number on test coverage, with minimum effort as possible, I hope this will show you some idea you can use :)<p>In my opinion, unit test is not that important (like must must have).
|
||||
It's just make sure your code is running excatly as you intent it to be.
|
||||
If you don't think about edge case before, unit test won't help you.<h2><a id=user-content-first-rewrite-the-impossible-to-test-out class=anchor aria-hidden=true href=#first-rewrite-the-impossible-to-test-out><span aria-hidden=true class="octicon octicon-link"></span></a>First, rewrite the impossible (to test) out</h2><p>When I learn programming, I encounter very interesting idea, which become mainly my mindset when I dev later.
|
||||
I don't recall it clearly, kinda like: "Don't just fix bugs, rewrite it so that kind of bugs will not appear again".
|
||||
So in our context, there is some thing we hardly or can not write test in Go.
|
||||
My suggestion is don't use that thing.<p>In my experience, I can list a few here:<ul><li>Read config each time call func (<code>viper.Get...</code>). You can and you should init all config when project starts.<li>Not use Dependency Injection (DI). There are too many posts in Internet tell you how to do DI properly.<li>Use global var (Except global var <code>Err...</code>). You should move all global var to fields inside some struct.</ul><h2><a id=user-content-let-the-fun-writing-test-begin class=anchor aria-hidden=true href=#let-the-fun-writing-test-begin><span aria-hidden=true class="octicon octicon-link"></span></a>Let the fun (writing test) begin</h2><p>If you code Go long enough, you know table driven tests and how is that so useful.
|
||||
You set up test data, then you test.
|
||||
Somewhere in the future, you change the func, then you need to update test data, then you good!<p>In simple case, your func only have 2 or 3 inputs so table drive tests is still looking good.
|
||||
But real world is ugly (maybe not, idk I'm just too young in this industry). Your func can have 5 or 10 inputs, also your func call many third party services.<p>Imagine having below func to upload image:<div class="highlight highlight-source-go"><pre><span class=pl-k>type</span> <span class=pl-smi>service</span> <span class=pl-k>struct</span> {
|
||||
<span class=pl-c1>db</span> <span class=pl-smi>DB</span>
|
||||
<span class=pl-c1>redis</span> <span class=pl-smi>Redis</span>
|
||||
<span class=pl-c1>minio</span> <span class=pl-smi>MinIO</span>
|
||||
<span class=pl-c1>logService</span> <span class=pl-smi>LogService</span>
|
||||
<span class=pl-c1>verifyService</span> <span class=pl-smi>VerifyService</span>
|
||||
}
|
||||
|
||||
func (s *service) Upload(ctx context.Context, req Request) error {
|
||||
// I simplify by omitting the response, only care error for now
|
||||
if err := s.verifyService.Verify(req); err != nil {
|
||||
return err
|
||||
<span class=pl-k>func</span> (<span class=pl-s1>s</span> <span class=pl-c1>*</span><span class=pl-smi>service</span>) <span class=pl-en>Upload</span>(<span class=pl-s1>ctx</span> context.<span class=pl-smi>Context</span>, <span class=pl-s1>req</span> <span class=pl-smi>Request</span>) <span class=pl-smi>error</span> {
|
||||
<span class=pl-c>// I simplify by omitting the response, only care error for now</span>
|
||||
<span class=pl-k>if</span> <span class=pl-s1>err</span> <span class=pl-c1>:=</span> <span class=pl-s1>s</span>.<span class=pl-c1>verifyService</span>.<span class=pl-en>Verify</span>(<span class=pl-s1>req</span>); <span class=pl-s1>err</span> <span class=pl-c1>!=</span> <span class=pl-c1>nil</span> {
|
||||
<span class=pl-k>return</span> <span class=pl-s1>err</span>
|
||||
}
|
||||
|
||||
if err := s.minio.Put(req); err != nil {
|
||||
return err
|
||||
<span class=pl-k>if</span> <span class=pl-s1>err</span> <span class=pl-c1>:=</span> <span class=pl-s1>s</span>.<span class=pl-c1>minio</span>.<span class=pl-en>Put</span>(<span class=pl-s1>req</span>); <span class=pl-s1>err</span> <span class=pl-c1>!=</span> <span class=pl-c1>nil</span> {
|
||||
<span class=pl-k>return</span> <span class=pl-s1>err</span>
|
||||
}
|
||||
|
||||
if err := s.redis.Set(req); err != nil {
|
||||
return err
|
||||
<span class=pl-k>if</span> <span class=pl-s1>err</span> <span class=pl-c1>:=</span> <span class=pl-s1>s</span>.<span class=pl-c1>redis</span>.<span class=pl-en>Set</span>(<span class=pl-s1>req</span>); <span class=pl-s1>err</span> <span class=pl-c1>!=</span> <span class=pl-c1>nil</span> {
|
||||
<span class=pl-k>return</span> <span class=pl-s1>err</span>
|
||||
}
|
||||
|
||||
if err := s.db.Save(req); err != nil {
|
||||
return err
|
||||
<span class=pl-k>if</span> <span class=pl-s1>err</span> <span class=pl-c1>:=</span> <span class=pl-s1>s</span>.<span class=pl-c1>db</span>.<span class=pl-en>Save</span>(<span class=pl-s1>req</span>); <span class=pl-s1>err</span> <span class=pl-c1>!=</span> <span class=pl-c1>nil</span> {
|
||||
<span class=pl-k>return</span> <span class=pl-s1>err</span>
|
||||
}
|
||||
|
||||
if err := s.logService.Save(req); err != nil {
|
||||
return err
|
||||
<span class=pl-k>if</span> <span class=pl-s1>err</span> <span class=pl-c1>:=</span> <span class=pl-s1>s</span>.<span class=pl-c1>logService</span>.<span class=pl-en>Save</span>(<span class=pl-s1>req</span>); <span class=pl-s1>err</span> <span class=pl-c1>!=</span> <span class=pl-c1>nil</span> {
|
||||
<span class=pl-k>return</span> <span class=pl-s1>err</span>
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
</code></pre><p>With table driven test and thanks to <a href=https://github.com/stretchr/testify>stretchr/testify</a>, I usually write like this:<pre><code class=language-go>type ServiceSuite struct {
|
||||
suite.Suite
|
||||
<span class=pl-k>return</span> <span class=pl-c1>nil</span>
|
||||
}</pre></div><p>With table driven test and thanks to <a href=https://github.com/stretchr/testify>stretchr/testify</a>, I usually write like this:<div class="highlight highlight-source-go"><pre><span class=pl-k>type</span> <span class=pl-smi>ServiceSuite</span> <span class=pl-k>struct</span> {
|
||||
suite.<span class=pl-smi>Suite</span>
|
||||
|
||||
db DBMock
|
||||
redis RedisMock
|
||||
minio MinIOMock
|
||||
logService LogServiceMock
|
||||
verifyService VerifyServiceMock
|
||||
<span class=pl-c1>db</span> <span class=pl-smi>DBMock</span>
|
||||
<span class=pl-c1>redis</span> <span class=pl-smi>RedisMock</span>
|
||||
<span class=pl-c1>minio</span> <span class=pl-smi>MinIOMock</span>
|
||||
<span class=pl-c1>logService</span> <span class=pl-smi>LogServiceMock</span>
|
||||
<span class=pl-c1>verifyService</span> <span class=pl-smi>VerifyServiceMock</span>
|
||||
|
||||
s service
|
||||
<span class=pl-c1>s</span> <span class=pl-smi>service</span>
|
||||
}
|
||||
|
||||
func (s *ServiceSuite) SetupTest() {
|
||||
// Init mock
|
||||
// Init service
|
||||
<span class=pl-k>func</span> (<span class=pl-s1>s</span> <span class=pl-c1>*</span><span class=pl-smi>ServiceSuite</span>) <span class=pl-en>SetupTest</span>() {
|
||||
<span class=pl-c>// Init mock</span>
|
||||
<span class=pl-c>// Init service</span>
|
||||
}
|
||||
|
||||
func (s *ServiceSuite) TestUpload() {
|
||||
tests := []struct{
|
||||
name string
|
||||
req Request
|
||||
verifyErr error
|
||||
minioErr error
|
||||
redisErr error
|
||||
dbErr error
|
||||
logErr error
|
||||
wantErr error
|
||||
<span class=pl-k>func</span> (<span class=pl-s1>s</span> <span class=pl-c1>*</span><span class=pl-smi>ServiceSuite</span>) <span class=pl-en>TestUpload</span>() {
|
||||
<span class=pl-s1>tests</span> <span class=pl-c1>:=</span> []<span class=pl-k>struct</span>{
|
||||
<span class=pl-c1>name</span> <span class=pl-smi>string</span>
|
||||
<span class=pl-c1>req</span> <span class=pl-smi>Request</span>
|
||||
<span class=pl-c1>verifyErr</span> <span class=pl-smi>error</span>
|
||||
<span class=pl-c1>minioErr</span> <span class=pl-smi>error</span>
|
||||
<span class=pl-c1>redisErr</span> <span class=pl-smi>error</span>
|
||||
<span class=pl-c1>dbErr</span> <span class=pl-smi>error</span>
|
||||
<span class=pl-c1>logErr</span> <span class=pl-smi>error</span>
|
||||
<span class=pl-c1>wantErr</span> <span class=pl-smi>error</span>
|
||||
}{
|
||||
{
|
||||
// Init test case
|
||||
<span class=pl-c>// Init test case</span>
|
||||
}
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
s.Run(tc.name, func(){
|
||||
// Mock all error depends on test case
|
||||
if tc.verifyErr != nil {
|
||||
s.verifyService.MockVerify().Return(tc.verifyErr)
|
||||
<span class=pl-k>for</span> <span class=pl-s1>_</span>, <span class=pl-s1>tc</span> <span class=pl-c1>:=</span> <span class=pl-k>range</span> <span class=pl-s1>tests</span> {
|
||||
<span class=pl-s1>s</span>.<span class=pl-en>Run</span>(<span class=pl-s1>tc</span>.<span class=pl-c1>name</span>, <span class=pl-k>func</span>(){
|
||||
<span class=pl-c>// Mock all error depends on test case</span>
|
||||
<span class=pl-k>if</span> <span class=pl-s1>tc</span>.<span class=pl-c1>verifyErr</span> <span class=pl-c1>!=</span> <span class=pl-c1>nil</span> {
|
||||
<span class=pl-s1>s</span>.<span class=pl-c1>verifyService</span>.<span class=pl-en>MockVerify</span>().<span class=pl-en>Return</span>(<span class=pl-s1>tc</span>.<span class=pl-c1>verifyErr</span>)
|
||||
}
|
||||
// ...
|
||||
<span class=pl-c>// ...</span>
|
||||
|
||||
gotErr := s.service.Upload(tc.req)
|
||||
s.Equal(wantErr, gotErr)
|
||||
<span class=pl-s1>gotErr</span> <span class=pl-c1>:=</span> <span class=pl-s1>s</span>.<span class=pl-c1>service</span>.<span class=pl-en>Upload</span>(<span class=pl-s1>tc</span>.<span class=pl-c1>req</span>)
|
||||
<span class=pl-s1>s</span>.<span class=pl-en>Equal</span>(<span class=pl-s1>wantErr</span>, <span class=pl-s1>gotErr</span>)
|
||||
})
|
||||
}
|
||||
}
|
||||
</code></pre><p>Looks good right?<br>Be careful with this.<br>It can go from 0 to 100 ugly real quick.<p>What if req is a struct with many fields?<br>So in each test case you need to set up req.<br>They are almost the same, but with some error case you must alter req.<br>It's easy to be init with wrong value here (typing maybe ?).<br>Also all req looks similiar, kinda duplicated.<pre><code class=language-go>tests := []struct{
|
||||
name string
|
||||
req Request
|
||||
verifyErr error
|
||||
minioErr error
|
||||
redisErr error
|
||||
dbErr error
|
||||
logErr error
|
||||
wantErr error
|
||||
}</pre></div><p>Looks good right?
|
||||
Be careful with this.
|
||||
It can go from 0 to 100 ugly real quick.<p>What if req is a struct with many fields?
|
||||
So in each test case you need to set up req.
|
||||
They are almost the same, but with some error case you must alter req.
|
||||
It's easy to be init with wrong value here (typing maybe ?).
|
||||
Also all req looks similiar, kinda duplicated.<div class="highlight highlight-source-go"><pre><span class=pl-s1>tests</span> <span class=pl-c1>:=</span> []<span class=pl-k>struct</span>{
|
||||
<span class=pl-c1>name</span> <span class=pl-smi>string</span>
|
||||
<span class=pl-c1>req</span> <span class=pl-smi>Request</span>
|
||||
<span class=pl-c1>verifyErr</span> <span class=pl-smi>error</span>
|
||||
<span class=pl-c1>minioErr</span> <span class=pl-smi>error</span>
|
||||
<span class=pl-c1>redisErr</span> <span class=pl-smi>error</span>
|
||||
<span class=pl-c1>dbErr</span> <span class=pl-smi>error</span>
|
||||
<span class=pl-c1>logErr</span> <span class=pl-smi>error</span>
|
||||
<span class=pl-c1>wantErr</span> <span class=pl-smi>error</span>
|
||||
}{
|
||||
{
|
||||
req: Request {
|
||||
a: "a",
|
||||
b: {
|
||||
c: "c",
|
||||
d: {
|
||||
"e": e
|
||||
<span class=pl-c1>req</span>: <span class=pl-smi>Request</span> {
|
||||
<span class=pl-c1>a</span>: <span class=pl-s>"a"</span>,
|
||||
<span class=pl-c1>b</span>: {
|
||||
<span class=pl-c1>c</span>: <span class=pl-s>"c"</span>,
|
||||
<span class=pl-c1>d</span>: {
|
||||
<span class=pl-s>"e"</span>: <span class=pl-s1>e</span>
|
||||
}
|
||||
}
|
||||
}
|
||||
// Other fieles
|
||||
<span class=pl-c>// Other fieles</span>
|
||||
},
|
||||
{
|
||||
req: Request {
|
||||
a: "a",
|
||||
b: {
|
||||
c: "c",
|
||||
d: {
|
||||
"e": e
|
||||
<span class=pl-c1>req</span>: <span class=pl-smi>Request</span> {
|
||||
<span class=pl-c1>a</span>: <span class=pl-s>"a"</span>,
|
||||
<span class=pl-c1>b</span>: {
|
||||
<span class=pl-c1>c</span>: <span class=pl-s>"c"</span>,
|
||||
<span class=pl-c1>d</span>: {
|
||||
<span class=pl-s>"e"</span>: <span class=pl-s1>e</span>
|
||||
}
|
||||
}
|
||||
}
|
||||
// Other fieles
|
||||
<span class=pl-c>// Other fieles</span>
|
||||
},
|
||||
{
|
||||
req: Request {
|
||||
a: "a",
|
||||
b: {
|
||||
c: "c",
|
||||
d: {
|
||||
"e": e
|
||||
<span class=pl-c1>req</span>: <span class=pl-smi>Request</span> {
|
||||
<span class=pl-c1>a</span>: <span class=pl-s>"a"</span>,
|
||||
<span class=pl-c1>b</span>: {
|
||||
<span class=pl-c1>c</span>: <span class=pl-s>"c"</span>,
|
||||
<span class=pl-c1>d</span>: {
|
||||
<span class=pl-s>"e"</span>: <span class=pl-s1>e</span>
|
||||
}
|
||||
}
|
||||
}
|
||||
// Other fieles
|
||||
<span class=pl-c>// Other fieles</span>
|
||||
}
|
||||
}
|
||||
</code></pre><p>What if dependencies of service keep growing?<br>More mock error to test data of course.<pre><code class=language-go> tests := []struct{
|
||||
name string
|
||||
req Request
|
||||
verifyErr error
|
||||
minioErr error
|
||||
redisErr error
|
||||
dbErr error
|
||||
logErr error
|
||||
wantErr error
|
||||
// Murr error
|
||||
aErr error
|
||||
bErr error
|
||||
cErr error
|
||||
// ...
|
||||
}</pre></div><p>What if dependencies of service keep growing?
|
||||
More mock error to test data of course.<div class="highlight highlight-source-go"><pre> <span class=pl-s1>tests</span> <span class=pl-c1>:=</span> []<span class=pl-k>struct</span>{
|
||||
<span class=pl-c1>name</span> <span class=pl-smi>string</span>
|
||||
<span class=pl-c1>req</span> <span class=pl-smi>Request</span>
|
||||
<span class=pl-c1>verifyErr</span> <span class=pl-smi>error</span>
|
||||
<span class=pl-c1>minioErr</span> <span class=pl-smi>error</span>
|
||||
<span class=pl-c1>redisErr</span> <span class=pl-smi>error</span>
|
||||
<span class=pl-c1>dbErr</span> <span class=pl-smi>error</span>
|
||||
<span class=pl-c1>logErr</span> <span class=pl-smi>error</span>
|
||||
<span class=pl-c1>wantErr</span> <span class=pl-smi>error</span>
|
||||
<span class=pl-c>// Murr error</span>
|
||||
<span class=pl-c1>aErr</span> <span class=pl-smi>error</span>
|
||||
<span class=pl-c1>bErr</span> <span class=pl-smi>error</span>
|
||||
<span class=pl-c1>cErr</span> <span class=pl-smi>error</span>
|
||||
<span class=pl-c>// ...</span>
|
||||
}{
|
||||
{
|
||||
// Init test case
|
||||
<span class=pl-c>// Init test case</span>
|
||||
}
|
||||
}
|
||||
</code></pre><p>The test file keep growing longer and longer until I feel sick about it.<p>See <a href=https://github.com/tektoncd/pipeline/blob/main/pkg/pod/pod_test.go>tektoncd/pipeline unit test</a> to get a feeling about this.<br>When I see it, <code>TestPodBuild</code> has almost 2000 lines.<p>The solution I propose here is simple (absolutely not perfect, but good with my usecase) thanks to <strong>stretchr/testify</strong>.<br>I init all <strong>default</strong> action on <strong>success</strong> case.<br>Then I <strong>alter</strong> request or mock error for unit test to hit on other case.<br>Remember if unit test is hit, code coverate is surely increaesed, and that my <strong>goal</strong>.<pre><code class=language-go>// Init ServiceSuite as above
|
||||
}</pre></div><p>The test file keep growing longer and longer until I feel sick about it.<p>See <a href=https://github.com/tektoncd/pipeline/blob/main/pkg/pod/pod_test.go>tektoncd/pipeline unit test</a> to get a feeling about this.
|
||||
When I see it, <code>TestPodBuild</code> has almost 2000 lines.<p>The solution I propose here is simple (absolutely not perfect, but good with my usecase) thanks to <strong>stretchr/testify</strong>.
|
||||
I init all <strong>default</strong> action on <strong>success</strong> case.
|
||||
Then I <strong>alter</strong> request or mock error for unit test to hit on other case.
|
||||
Remember if unit test is hit, code coverate is surely increaesed, and that my <strong>goal</strong>.<div class="highlight highlight-source-go"><pre><span class=pl-c>// Init ServiceSuite as above</span>
|
||||
|
||||
func (s *ServiceSuite) TestUpload() {
|
||||
// Init success request
|
||||
req := Request{
|
||||
// ...
|
||||
<span class=pl-k>func</span> (<span class=pl-s1>s</span> <span class=pl-c1>*</span><span class=pl-smi>ServiceSuite</span>) <span class=pl-en>TestUpload</span>() {
|
||||
<span class=pl-c>// Init success request</span>
|
||||
<span class=pl-s1>req</span> <span class=pl-c1>:=</span> <span class=pl-smi>Request</span>{
|
||||
<span class=pl-c>// ...</span>
|
||||
}
|
||||
|
||||
// Init success action
|
||||
s.verifyService.MockVerify().Return(nil)
|
||||
// ...
|
||||
<span class=pl-c>// Init success action</span>
|
||||
<span class=pl-s1>s</span>.<span class=pl-c1>verifyService</span>.<span class=pl-en>MockVerify</span>().<span class=pl-en>Return</span>(<span class=pl-c1>nil</span>)
|
||||
<span class=pl-c>// ...</span>
|
||||
|
||||
gotErr := s.service.Upload(tc.req)
|
||||
s.NoError(gotErr)
|
||||
<span class=pl-s1>gotErr</span> <span class=pl-c1>:=</span> <span class=pl-s1>s</span>.<span class=pl-c1>service</span>.<span class=pl-en>Upload</span>(<span class=pl-s1>tc</span>.<span class=pl-c1>req</span>)
|
||||
<span class=pl-s1>s</span>.<span class=pl-en>NoError</span>(<span class=pl-s1>gotErr</span>)
|
||||
|
||||
s.Run("failed", func(){
|
||||
// Alter failed request from default
|
||||
req := Request{
|
||||
// ...
|
||||
<span class=pl-s1>s</span>.<span class=pl-en>Run</span>(<span class=pl-s>"failed"</span>, <span class=pl-k>func</span>(){
|
||||
<span class=pl-c>// Alter failed request from default</span>
|
||||
<span class=pl-s1>req</span> <span class=pl-c1>:=</span> <span class=pl-smi>Request</span>{
|
||||
<span class=pl-c>// ...</span>
|
||||
}
|
||||
|
||||
gotErr := s.service.Upload(tc.req)
|
||||
s.Error(gotErr)
|
||||
<span class=pl-s1>gotErr</span> <span class=pl-c1>:=</span> <span class=pl-s1>s</span>.<span class=pl-c1>service</span>.<span class=pl-en>Upload</span>(<span class=pl-s1>tc</span>.<span class=pl-c1>req</span>)
|
||||
<span class=pl-s1>s</span>.<span class=pl-en>Error</span>(<span class=pl-s1>gotErr</span>)
|
||||
})
|
||||
|
||||
s.Run("another failed", func(){
|
||||
// Alter verify return
|
||||
s.verifyService.MockVerify().Return(someErr)
|
||||
<span class=pl-s1>s</span>.<span class=pl-en>Run</span>(<span class=pl-s>"another failed"</span>, <span class=pl-k>func</span>(){
|
||||
<span class=pl-c>// Alter verify return</span>
|
||||
<span class=pl-s1>s</span>.<span class=pl-c1>verifyService</span>.<span class=pl-en>MockVerify</span>().<span class=pl-en>Return</span>(<span class=pl-s1>someErr</span>)
|
||||
|
||||
|
||||
gotErr := s.service.Upload(tc.req)
|
||||
s.Error(gotErr)
|
||||
<span class=pl-s1>gotErr</span> <span class=pl-c1>:=</span> <span class=pl-s1>s</span>.<span class=pl-c1>service</span>.<span class=pl-en>Upload</span>(<span class=pl-s1>tc</span>.<span class=pl-c1>req</span>)
|
||||
<span class=pl-s1>s</span>.<span class=pl-en>Error</span>(<span class=pl-s1>gotErr</span>)
|
||||
})
|
||||
|
||||
// ...
|
||||
}
|
||||
</code></pre><p>If you think this is not quick enough, just <strong>ignore</strong> the response.<br>You only need to check error or not if you want code coverage only.<p>So if request change fields or more dependencies, I need to update success case, and maybe add corresponding error case if need.<p>Same idea but still with table, you can find here <a href=https://arslan.io/2022/12/04/functional-table-driven-tests-in-go/>Functional table-driven tests in Go - Fatih Arslan</a>.</p><a href=mailto:hauvipapro+posts@gmail.com>Feel free to ask me via email</a>
|
||||
<span class=pl-c>// ...</span>
|
||||
}</pre></div><p>If you think this is not quick enough, just <strong>ignore</strong> the response.
|
||||
You only need to check error or not if you want code coverage only.<p>So if request change fields or more dependencies, I need to update success case, and maybe add corresponding error case if need.<p>Same idea but still with table, you can find here <a href=https://arslan.io/2022/12/04/functional-table-driven-tests-in-go/ rel=nofollow>Functional table-driven tests in Go - Fatih Arslan</a>.</p><a href=mailto:hauvipapro+posts@gmail.com>Feel free to ask me via email</a>
|
||||
<a rel=me href=https://hachyderm.io/@haunguyen>Mastodon</a>
|
|
@ -1,2 +1,2 @@
|
|||
<!doctype html><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><link rel=preconnect href=https://fonts.googleapis.com><link rel=preconnect href=https://fonts.gstatic.com crossorigin><link href="https://fonts.googleapis.com/css2?family=Recursive:wght,CASL,MONO@300..800,0..1,0..1&display=swap" rel=stylesheet><link href=https://haunt98.github.io/iosevka_webfont/iosevka-term-ss08/iosevka-term-ss08.css rel=stylesheet><link rel=stylesheet href=styles.css><a href=index>Index</a><h1>Index</h1><p>This is where I dump my thoughts.<ul><li><a href=2022-06-08-backup>Backup my way</a><li><a href=2022-06-08-dockerfile-go>Dockerfile for Go</a><li><a href=2022-07-10-bootstrap-go>Bootstrap Go</a><li><a href=2022-07-12-uuid-or-else>UUID or else</a><li><a href=2022-07-19-migrate-to-buf>Migrate to buf</a><li><a href=2022-07-31-sql>SQL</a><li><a href=2022-07-31-experiment-go>Experiment go</a><li><a href=2022-08-10-gitignore>gitignore</a><li><a href=2022-10-26-reload-config>Reload config</a><li><a href=2022-12-25-archlinux>Install Arch Linux</a><li><a href=2022-12-25-go-test-asap>Speed up writing Go test ASAP</a></ul><a href=mailto:hauvipapro+posts@gmail.com>Feel free to ask me via email</a>
|
||||
<!doctype html><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><link rel=stylesheet href=https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.1.0/github-markdown-dark.css><style>.markdown-body{box-sizing:border-box;min-width:200px;max-width:980px;margin:0 auto;padding:45px}@media(max-width:767px){.markdown-body{padding:15px}}</style><body class=markdown-body><a href=index>Index</a><h1><a id=user-content-index class=anchor aria-hidden=true href=#index><span aria-hidden=true class="octicon octicon-link"></span></a>Index</h1><p>This is where I dump my thoughts.<ul><li><a href=2022-06-08-backup>Backup my way</a><li><a href=2022-06-08-dockerfile-go>Dockerfile for Go</a><li><a href=2022-07-10-bootstrap-go>Bootstrap Go</a><li><a href=2022-07-12-uuid-or-else>UUID or else</a><li><a href=2022-07-19-migrate-to-buf>Migrate to buf</a><li><a href=2022-07-31-sql>SQL</a><li><a href=2022-07-31-experiment-go>Experiment go</a><li><a href=2022-08-10-gitignore>gitignore</a><li><a href=2022-10-26-reload-config>Reload config</a><li><a href=2022-12-25-archlinux>Install Arch Linux</a><li><a href=2022-12-25-go-test-asap>Speed up writing Go test ASAP</a></ul><a href=mailto:hauvipapro+posts@gmail.com>Feel free to ask me via email</a>
|
||||
<a rel=me href=https://hachyderm.io/@haunguyen>Mastodon</a>
|
13
go.mod
13
go.mod
|
@ -3,8 +3,17 @@ module github.com/haunt98/posts-go
|
|||
go 1.18
|
||||
|
||||
require (
|
||||
github.com/google/go-github/v48 v48.2.0
|
||||
github.com/tdewolff/minify/v2 v2.12.4
|
||||
github.com/yuin/goldmark v1.5.3
|
||||
golang.org/x/oauth2 v0.3.0
|
||||
)
|
||||
|
||||
require github.com/tdewolff/parse/v2 v2.6.4 // indirect
|
||||
require (
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/tdewolff/parse/v2 v2.6.4 // indirect
|
||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect
|
||||
golang.org/x/net v0.3.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/protobuf v1.28.0 // indirect
|
||||
)
|
||||
|
|
32
go.sum
32
go.sum
|
@ -2,6 +2,17 @@ github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927/go.mod h1:h/aW8ynjgk
|
|||
github.com/djherbis/atime v1.1.0/go.mod h1:28OF6Y8s3NQWwacXc5eZTsEsiMzp7LF8MbXE+XJPdBE=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-github/v48 v48.2.0 h1:68puzySE6WqUY9KWmpOsDEQfDZsso98rT6pZcz9HqcE=
|
||||
github.com/google/go-github/v48 v48.2.0/go.mod h1:dDlehKBDo850ZPvCTK0sEqTCVWcrGl2LcDiajkYi89Y=
|
||||
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
||||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
||||
github.com/matryer/try v0.0.0-20161228173917-9ac251b645a2/go.mod h1:0KeJpeMD6o+O4hW7qJOT7vyQPKrWmj26uf5wMc/IiIs=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/tdewolff/minify/v2 v2.12.4 h1:kejsHQMM17n6/gwdw53qsi6lg0TGddZADVyQOz1KMdE=
|
||||
|
@ -10,6 +21,23 @@ github.com/tdewolff/parse/v2 v2.6.4 h1:KCkDvNUMof10e3QExio9OPZJT8SbdKojLBumw8YZy
|
|||
github.com/tdewolff/parse/v2 v2.6.4/go.mod h1:woz0cgbLwFdtbjJu8PIKxhW05KplTFQkOdX78o+Jgrs=
|
||||
github.com/tdewolff/test v1.0.7 h1:8Vs0142DmPFW/bQeHRP3MV19m1gvndjUb1sn8yy74LM=
|
||||
github.com/tdewolff/test v1.0.7/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=
|
||||
github.com/yuin/goldmark v1.5.3 h1:3HUJmBFbQW9fhQOzMgseU134xfi6hU+mjWywx5Ty+/M=
|
||||
github.com/yuin/goldmark v1.5.3/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ=
|
||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.3.0 h1:VWL6FNY2bEEmsGVKabSlHu5Irp34xmMRoqb/9lF9lxk=
|
||||
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
|
||||
golang.org/x/oauth2 v0.3.0 h1:6l90koy8/LaBLmLu8jpHeHexzMwEita0zFfYlggy2F8=
|
||||
golang.org/x/oauth2 v0.3.0/go.mod h1:rQrIauxkUhJ6CuwEXwymO2/eh4xz2ZWF1nBkcxS+tGk=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
|
||||
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
|
|
85
main.go
85
main.go
|
@ -1,20 +1,15 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"context"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/tdewolff/minify/v2"
|
||||
minify_css "github.com/tdewolff/minify/v2/css"
|
||||
minify_html "github.com/tdewolff/minify/v2/html"
|
||||
"github.com/yuin/goldmark"
|
||||
gm_extension "github.com/yuin/goldmark/extension"
|
||||
gm_html "github.com/yuin/goldmark/renderer/html"
|
||||
"github.com/google/go-github/v48/github"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -37,6 +32,8 @@ type templatePostData struct {
|
|||
}
|
||||
|
||||
func main() {
|
||||
ctx := context.Background()
|
||||
|
||||
// Cleanup generated path
|
||||
if err := os.RemoveAll(generatedPath); err != nil {
|
||||
log.Fatalln("Failed to remove all", generatedPath, err)
|
||||
|
@ -63,20 +60,19 @@ func main() {
|
|||
log.Fatalln("Failed to parse template", err)
|
||||
}
|
||||
|
||||
// Prepare parse markdown
|
||||
gm := goldmark.New(
|
||||
goldmark.WithExtensions(
|
||||
gm_extension.GFM,
|
||||
),
|
||||
goldmark.WithRendererOptions(
|
||||
gm_html.WithHardWraps(),
|
||||
),
|
||||
)
|
||||
// Prepare GitHub
|
||||
ghAccessTokenBytes, err := os.ReadFile(".github_access_token")
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to read file", ".github_access_token", err)
|
||||
}
|
||||
|
||||
// Prepare minify
|
||||
m := minify.New()
|
||||
m.AddFunc(mimeTypeHTML, minify_html.Minify)
|
||||
m.AddFunc(mimeTypeCSS, minify_css.Minify)
|
||||
ghTokenSrc := oauth2.StaticTokenSource(
|
||||
&oauth2.Token{
|
||||
AccessToken: string(ghAccessTokenBytes),
|
||||
},
|
||||
)
|
||||
ghHTTPClient := oauth2.NewClient(ctx, ghTokenSrc)
|
||||
ghClient := github.NewClient(ghHTTPClient)
|
||||
|
||||
// Generate post files
|
||||
for _, postFile := range postFiles {
|
||||
|
@ -91,6 +87,13 @@ func main() {
|
|||
log.Fatalln("Failed to read file", mdFilename, err)
|
||||
}
|
||||
|
||||
ghMarkdown, _, err := ghClient.Markdown(ctx, string(mdFileBytes), &github.MarkdownOptions{
|
||||
Mode: "markdown",
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to GitHub markdown", err)
|
||||
}
|
||||
|
||||
// Prepare html file
|
||||
htmlFilename := strings.TrimSuffix(postFile.Name(), filepath.Ext(postFile.Name())) + extHTML
|
||||
htmlFilepath := filepath.Join(generatedPath, htmlFilename)
|
||||
|
@ -100,45 +103,23 @@ func main() {
|
|||
log.Fatalln("Failed to open file", htmlFilepath, err)
|
||||
}
|
||||
|
||||
// Parse markdown
|
||||
var markdownBuf bytes.Buffer
|
||||
if err := gm.Convert(mdFileBytes, &markdownBuf); err != nil {
|
||||
log.Fatalln("Failed to convert markdown", err)
|
||||
if err := templatePost.Execute(htmlFile, templatePostData{
|
||||
Body: ghMarkdown,
|
||||
}); err != nil {
|
||||
log.Fatalln("Failed to execute html template", err)
|
||||
}
|
||||
|
||||
tmpReader, tmpWriter := io.Pipe()
|
||||
|
||||
// Template
|
||||
go func() {
|
||||
if err := templatePost.Execute(tmpWriter, templatePostData{
|
||||
Body: markdownBuf.String(),
|
||||
}); err != nil {
|
||||
log.Fatalln("Failed to execute html template", err)
|
||||
}
|
||||
tmpWriter.Close()
|
||||
}()
|
||||
|
||||
// Minify
|
||||
if err := m.Minify(mimeTypeHTML, htmlFile, tmpReader); err != nil {
|
||||
log.Fatalln("Failed to minify html", err)
|
||||
}
|
||||
tmpReader.Close()
|
||||
htmlFile.Close()
|
||||
}
|
||||
|
||||
// Copy css file
|
||||
templateCSSFile, err := os.OpenFile(templateCSSPath, os.O_RDONLY, 0o600)
|
||||
// Copy css file from templates to generated
|
||||
templateCSSBytes, err := os.ReadFile(templateCSSPath)
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to open file", templateCSSPath, err)
|
||||
}
|
||||
|
||||
cssFilename := filepath.Join(generatedPath, cssFilename)
|
||||
cssFile, err := os.OpenFile(cssFilename, os.O_RDWR|os.O_CREATE, 0o600)
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to open file", cssFilename, err)
|
||||
}
|
||||
|
||||
if err := m.Minify(mimeTypeCSS, cssFile, templateCSSFile); err != nil {
|
||||
log.Fatalln("Failed to minify css", err)
|
||||
generatedCSSPath := filepath.Join(generatedPath, cssFilename)
|
||||
if err := os.WriteFile(generatedCSSPath, templateCSSBytes, 0o600); err != nil {
|
||||
log.Fatalln("Failed to write file", generatedCSSPath, err)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,16 +6,21 @@
|
|||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Recursive:wght,CASL,MONO@300..800,0..1,0..1&display=swap"
|
||||
rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css2?family=Recursive:wght,CASL,MONO@300..800,0..1,0..1&display=swap"
|
||||
/>
|
||||
<link
|
||||
href="https://haunt98.github.io/iosevka_webfont/iosevka-term-ss08/iosevka-term-ss08.css"
|
||||
rel="stylesheet"
|
||||
href="https://haunt98.github.io/iosevka_webfont/iosevka-term-ss08/iosevka-term-ss08.css"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.1.0/github-markdown.min.css"
|
||||
/>
|
||||
<link rel="stylesheet" href="styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
<style></style>
|
||||
<body class="markdown-body">
|
||||
<a href="index">Index</a>
|
||||
{{.Body}}
|
||||
<a href="mailto:hauvipapro+posts@gmail.com"
|
||||
|
|
|
@ -1,3 +1,19 @@
|
|||
/* https://github.com/sindresorhus/github-markdown-css */
|
||||
.markdown-body {
|
||||
box-sizing: border-box;
|
||||
min-width: 200px;
|
||||
max-width: 980px;
|
||||
margin: 0 auto;
|
||||
padding: 45px;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.markdown-body {
|
||||
padding: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Custom style */
|
||||
body {
|
||||
font-family: "Recursive", sans-serif;
|
||||
font-variation-settings: "MONO" 0, "CASL" 1;
|
||||
|
@ -13,26 +29,3 @@ code {
|
|||
display: inline-block;
|
||||
background: #f6f8fa;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #0969da;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
body {
|
||||
color: #c9d1d9;
|
||||
background: #0d1117;
|
||||
}
|
||||
|
||||
code {
|
||||
background: #161b22;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #58a6ff;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue