diff --git a/.github/workflows/go_cross.yml b/.github/workflows/go_cross.yml index f66ce454..620340ea 100644 --- a/.github/workflows/go_cross.yml +++ b/.github/workflows/go_cross.yml @@ -7,26 +7,23 @@ on: branches: ["main"] workflow_dispatch: - concurrency: group: ${{ github.workflow }}-${{ github.ref}} cancel-in-progress: true - jobs: - build: strategy: matrix: - go-version: [ '1.18.x', '1.19.x', '1.20.x', '1.21.x'] - arch: [ x64, arm, arm64 ] - os: [ macos-latest, ubuntu-latest ] #windows-latest + go-version: ["1.19.x", "1.20.x", "1.21.x", "1.22.x"] + arch: [x64, arm, arm64] + os: [macos-latest, ubuntu-latest] #windows-latest include: - os: ubuntu-latest gocache: /tmp/go/gocache -# - os: windows-latest -# gocache: C:/gocache + # - os: windows-latest + # gocache: C:/gocache - os: macos-latest gocache: /tmp/go/gocache @@ -40,7 +37,6 @@ jobs: env: GOCACHE: ${{matrix.gocache}} - steps: - uses: actions/checkout@v3 @@ -50,15 +46,14 @@ jobs: go-version: ${{ matrix.go-version }} check-latest: true - - name: Cache Go tests - uses: actions/cache@v3 - with: - path: | - ${{env.GOCACHE}} - key: ${{ github.workflow }}-${{ runner.os }}-${{ matrix.arch }}-go-${{matrix.go-version}}-${{ hashFiles('**/go.mod','*_test.go') }} - restore-keys: | - ${{ github.workflow }}-${{ runner.os }}-${{ matrix.arch }}-go-${{matrix.go-version}}-${{ hashFiles('**/go.mod','*_test.go') }} - + # - name: Cache Go tests + # uses: actions/cache@v3 + # with: + # path: | + # ${{env.GOCACHE}} + # key: ${{ github.workflow }}-${{ runner.os }}-${{ matrix.arch }}-go-${{matrix.go-version}}-${{ hashFiles('**/go.mod','*_test.go') }} + # restore-keys: | + # ${{ github.workflow }}-${{ runner.os }}-${{ matrix.arch }}-go-${{matrix.go-version}}-${{ hashFiles('**/go.mod','*_test.go') }} - name: Linter continue-on-error: true diff --git a/.github/workflows/go_fuzz.yml b/.github/workflows/go_fuzz.yml index a5968542..f8ef8192 100644 --- a/.github/workflows/go_fuzz.yml +++ b/.github/workflows/go_fuzz.yml @@ -3,16 +3,15 @@ name: Go Fuzz on: push: branches: - - '*' - - '**' + - "*" + - "**" pull_request: branches: - - 'main' + - "main" workflow_dispatch: schedule: - - cron: '0 * */1 * *' - + - cron: "0 * */1 * *" concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -38,15 +37,15 @@ jobs: check-latest: true cache-dependency-path: go.sum - - name: Cache Go - uses: actions/cache@v3 - with: - path: | - ${{ env.GOCACHE }} - ${{ env.GOBIN }} - key: ${{ github.workflow }}-${{ runner.os }}-${{ hashFiles('*_test.go') }} - restore-keys: | - ${{ github.workflow }}-${{ runner.os }}-${{ hashFiles('*_test.go') }} + # - name: Cache Go + # uses: actions/cache@v3 + # with: + # path: | + # ${{ env.GOCACHE }} + # ${{ env.GOBIN }} + # key: ${{ github.workflow }}-${{ runner.os }}-${{ hashFiles('*_test.go') }} + # restore-keys: | + # ${{ github.workflow }}-${{ runner.os }}-${{ hashFiles('*_test.go') }} - name: Build timeout-minutes: 2 @@ -58,7 +57,6 @@ jobs: run: | go generate # go generate fuzz.go - - name: Upload Artifacts uses: actions/upload-artifact@v3 with: @@ -68,12 +66,10 @@ jobs: - name: Test Fuzz Functions run: | go test -cover -covermode=atomic -timeout=8m -race -run="Fuzz*" -json -short | \ - tparse -follow -all -sort=elapsed - - + tparse -follow -all -sort=elapsed fuzz: - needs: [ setup ] + needs: [setup] runs-on: ubuntu-latest env: @@ -106,8 +102,6 @@ jobs: name: go-test-utils path: ${{ env.GOBIN }} - - - run: chmod +x ${{ env.GOBIN }}/* - name: FuzzMultiWrite @@ -153,7 +147,7 @@ jobs: git push else echo "All Fuzz Tests have passed" - fi + fi - name: Upload TestCases uses: actions/upload-artifact@v2 @@ -173,14 +167,6 @@ jobs: echo "fuzz tests have passed on 2nd run" fi - - - - - - - - # fuzz: # needs: [ build, fuzz-multiwrite ] #will fail if more than 1 fuzz function is present # runs-on: ubuntu-latest @@ -228,4 +214,4 @@ jobs: # Fails if multiple Fuzz Functions match # go test -fuzz=Fuzz -fuzztime=30s -cover -covermode=count -run="Fuzz*" -json -short | \ -# tparse -follow -all -sort=elapsed \ No newline at end of file +# tparse -follow -all -sort=elapsed diff --git a/.gitignore b/.gitignore index 77ebb1b3..f87194ea 100644 --- a/.gitignore +++ b/.gitignore @@ -26,4 +26,4 @@ cover.txt # Created by go-fuzz -testdata/ \ No newline at end of file +testdata/* diff --git a/.golangci.yaml b/.golangci.yaml index a7fa4fd9..1fbcdc97 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -1,12 +1,4 @@ linters-settings: - depguard: - list-type: denylist - packages: - # logging is allowed only by logutils.Log, logrus - # is allowed to use only in logutils package - - github.com/sirupsen/logrus - packages-with-error-message: - - github.com/sirupsen/logrus: "logging is allowed only by logutils.Log" dupl: threshold: 100 funlen: @@ -32,7 +24,7 @@ linters-settings: min-complexity: 15 goimports: local-prefixes: github.com/golangci/golangci-lint - gomnd: + mnd: # don't include the "operation" and "assign" checks: - argument @@ -47,21 +39,11 @@ linters-settings: ignored-functions: - strings.SplitN - govet: - check-shadowing: true - settings: - printf: - funcs: - - (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof - - (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf - - (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf - - (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf lll: line-length: 140 misspell: locale: US nolintlint: - allow-leading-space: true # don't require machine-readable nolint directives (i.e. with no leading space) allow-unused: false # report any unused nolint directives require-explanation: false # don't require an explanation for nolint directives require-specific: false # don't require nolint directives to be specific about which linter is being skipped @@ -88,7 +70,7 @@ linters: - gocyclo - gofmt - goimports - - gomnd + - mnd - goprintffuncname - gosec - gosimple @@ -132,6 +114,8 @@ issues: - path: _test\.go linters: - gomnd + - revive + - depguard - path: db_test.go text: "deferInLoop: Possible resource leak, 'defer' is called in the 'for' loop" @@ -169,5 +153,4 @@ issues: run: timeout: 5m - go: "1.18" - skip-dirs: [examples] + go: "1.22" diff --git a/Makefile b/Makefile index a609ca54..75454f55 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,8 @@ export PATH := $(PWD)/bin:$(PATH) # Default Shell export SHELL := bash # Type of OS: Linux or Darwin. -export OSTYPE := $(shell uname -s) +export OSTYPE := $(shell uname -s | tr A-Z a-z) +export ARCH := $(shell uname -m) ifeq ($(OSTYPE),Darwin) export MallocNanoZone=0 diff --git a/README.md b/README.md index 6840688b..783bb2c4 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,15 @@ Golang Database Resolver and Wrapper for any multiple database connections topol This DBResolver library will split your connections to correct defined DBs. Eg, all read query will routed to ReadOnly replica db, and all write operation(Insert, Update, Delete) will routed to Primary/Master DB. -Read more for the explanation on this [blog post](https://betterprogramming.pub/create-a-cross-region-rdbms-connection-library-with-dbresolver-5072bed6a7b8) +**Read More** +|Items| Link| +------|-----| +|Blogpost| [blog post](https://betterprogramming.pub/create-a-cross-region-rdbms-connection-library-with-dbresolver-5072bed6a7b8) | +|Excalidraw| [diagram](https://excalidraw.com/#json=DTs8yxHOGF6uLkjnZny4z,RVo8iwhO0Rk6DRGkKuNZTg)| +|GoSG Meetup Demo| [repository](https://github.com/bxcodec/dbresolver-examples) | +| GoSG Presentation | [deck](https://www.canva.com/design/DAFgbpc7tfw/bEXVFtcHEnlFxKVBdnUggA/edit?utm_content=DAFgbpc7tfw&utm_campaign=designshare&utm_medium=link2&utm_source=sharebutton) | +| Instagram | [post](https://www.instagram.com/p/CnlDFPsBAJG/?utm_source=ig_web_copy_link&igsh=MzRlODBiNWFlZA==)| -Excalidraw live [diagram](https://excalidraw.com/#json=DTs8yxHOGF6uLkjnZny4z,RVo8iwhO0Rk6DRGkKuNZTg) ### Usecase 1: Separated RW and RO Database connection
@@ -64,7 +70,7 @@ go get -u github.com/bxcodec/dbresolver/v2 # Example -### Implementing DB Resolver using *sql.DB +### Implementing DB Resolver using \*sql.DB
@@ -152,6 +158,4 @@ func main() { ## Contribution ---- - To contrib to this project, you can open a PR or an issue. diff --git a/db.go b/db.go index 32d17c5b..27da45ee 100644 --- a/db.go +++ b/db.go @@ -184,7 +184,7 @@ func (db *sqlDB) PrepareContext(ctx context.Context, query string) (_stmt Stmt, err = multierr.Combine(errPrimaries, errReplicas) if err != nil { - return + return //nolint: nakedret } _query := strings.ToUpper(query) diff --git a/examples/example_wrap_dbs_test.go b/examples/example_wrap_dbs_test.go deleted file mode 100644 index 04c21338..00000000 --- a/examples/example_wrap_dbs_test.go +++ /dev/null @@ -1,58 +0,0 @@ -package examples_test - -import ( - "context" - "database/sql" - "fmt" - "log" - - "github.com/bxcodec/dbresolver/v2" - _ "github.com/lib/pq" -) - -func ExampleNew() { - var ( - host1 = "localhost" - port1 = 5432 - user1 = "postgresrw" - password1 = "" - host2 = "localhost" - port2 = 5433 - user2 = "postgresro" - password2 = "" - dbname = "" - ) - // connection string - rwPrimary := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", host1, port1, user1, password1, dbname) - readOnlyReplica := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", host2, port2, user2, password2, dbname) - - // open database for primary - dbPrimary, err := sql.Open("postgres", rwPrimary) - if err != nil { - log.Print("go error when connecting to the DB") - } - // configure the DBs for other setup eg, tracing, etc - // eg, tracing.Postgres(dbPrimary) - - // open database for replica - dbReadOnlyReplica, err := sql.Open("postgres", readOnlyReplica) - if err != nil { - log.Print("go error when connecting to the DB") - } - // configure the DBs for other setup eg, tracing, etc - // eg, tracing.Postgres(dbReadOnlyReplica) - - connectionDB := dbresolver.New( - dbresolver.WithPrimaryDBs(dbPrimary), - dbresolver.WithReplicaDBs(dbReadOnlyReplica), - dbresolver.WithLoadBalancer(dbresolver.RoundRobinLB)) - - defer connectionDB.Close() - // now you can use the connection for all DB operation - _, err = connectionDB.ExecContext(context.Background(), "DELETE FROM book WHERE id=$1") // will use primaryDB - if err != nil { - log.Print("go error when executing the query to the DB", err) - } - connectionDB.QueryRowContext(context.Background(), "SELECT * FROM book WHERE id=$1") // will use replicaReadOnlyDB - // Output: -} diff --git a/examples/gosg-demo/Makefile b/examples/gosg-demo/Makefile deleted file mode 100644 index bf9fdd49..00000000 --- a/examples/gosg-demo/Makefile +++ /dev/null @@ -1,72 +0,0 @@ -# Exporting bin folder to the path for makefile -export PATH := $(PWD)/bin:$(PATH) -# Default Shell -export SHELL := bash -# Type of OS: Linux or Darwin. -export OSTYPE := $(shell uname -s) - -export MUSL := $(shell [ -x /sbin/apk ] && echo "-tags musl" || echo "") - -ifeq ($(OSTYPE),Darwin) - export MallocNanoZone=0 -endif - -define github_url - https://github.com/$(GITHUB_REPO)/releases/download/v$(VERSION)/$(ARCHIVE) -endef - -# creates a directory bin. -bin: - @ mkdir -p $@ - -# ~~~ Tools ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -# ~~ [migrate] ~~~ https://github.com/golang-migrate/migrate ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -MIGRATE := $(shell command -v migrate || echo "bin/migrate") -migrate: bin/migrate ## Install migrate (database migration) - -bin/migrate: VERSION := 4.14.1 -bin/migrate: GITHUB_REPO := golang-migrate/migrate -bin/migrate: ARCHIVE := migrate.$(OSTYPE)-amd64.tar.gz -bin/migrate: bin - @ printf "Install migrate... " - @ curl -Ls $(call github_url) | tar -zOxf - ./migrate.$(shell echo $(OSTYPE) | tr A-Z a-z)-amd64 > $@ && chmod +x $@ - @ echo "done." - - -# ~~~ Database Migrations ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -POSTGRES_USER ?= postgres -POSTGRES_PASSWORD ?= my_password -POSTGRES_HOST ?= localhost -POSTGRES_PORT ?= 5432 -POSTGRES_DATABASE ?= my_database - - -PG_DSN := "postgres://$(POSTGRES_USER):$(POSTGRES_PASSWORD)@$(POSTGRES_HOST):$(POSTGRES_PORT)/$(POSTGRES_DATABASE)?sslmode=disable" - -migrate-up: $(MIGRATE) ## Apply all (or N up) migrations. - @ read -p "How many migration you wants to perform (default value: [all]): " N; \ - migrate -database $(PG_DSN) -path=internal/postgres/migrations up $${N} -# if you encounter the dirty version, fix the error, then use the below command -# migrate -database $(PG_DSN) -path=internal/postgres/migrations force previous_version up - -.PHONY: migrate-down -migrate-down: $(MIGRATE) ## Apply all (or N down) migrations. - @ read -p "How many migration you wants to perform (default value: [all]): " N; \ - migrate -database $(PG_DSN) -path=internal/postgres/migrations down $${N} - -.PHONY: migrate-drop -migrate-drop: $(MIGRATE) ## Drop everything inside the database. - migrate -database $(PG_DSN) -path=internal/postgres/migrations drop - -.PHONY: migrate-create -migrate-create: $(MIGRATE) ## Create a set of up/down migrations with a specified name. - @ read -p "Please provide name for the migration: " Name; \ - migrate create -ext sql -dir internal/postgres/migrations $${Name} - -up: - @ docker-compose up -d -down: - @ docker-compose up -d \ No newline at end of file diff --git a/examples/gosg-demo/docker-compose.yaml b/examples/gosg-demo/docker-compose.yaml deleted file mode 100644 index 626bd8d4..00000000 --- a/examples/gosg-demo/docker-compose.yaml +++ /dev/null @@ -1,39 +0,0 @@ -version: "2" - -services: - postgres-rw: - image: "docker.io/bitnami/postgresql:11-debian-10" - ports: - - "5432:5432" - volumes: - - "postgresql_master_data:/bitnami/postgresql" - environment: - - POSTGRESQL_PGAUDIT_LOG=READ,WRITE - - POSTGRESQL_LOG_HOSTNAME=true - - POSTGRESQL_REPLICATION_MODE=master - - POSTGRESQL_REPLICATION_USER=repl_user - - POSTGRESQL_REPLICATION_PASSWORD=repl_password - - POSTGRESQL_USERNAME=postgres - - POSTGRESQL_DATABASE=my_database - # - ALLOW_EMPTY_PASSWORD=yes - - POSTGRESQL_PASSWORD=my_password - postgres-ro: - image: "docker.io/bitnami/postgresql:11-debian-10" - ports: - - "5433:5432" - depends_on: - - postgres-rw - environment: - - POSTGRESQL_USERNAME=postgres - - POSTGRESQL_PASSWORD=my_password - - POSTGRESQL_MASTER_HOST=postgres-rw - - POSTGRESQL_PGAUDIT_LOG=READ,WRITE - - POSTGRESQL_LOG_HOSTNAME=true - - POSTGRESQL_REPLICATION_MODE=slave - - POSTGRESQL_REPLICATION_USER=repl_user - - POSTGRESQL_REPLICATION_PASSWORD=repl_password - - POSTGRESQL_MASTER_PORT_NUMBER=5432 - -volumes: - postgresql_master_data: - driver: local diff --git a/examples/gosg-demo/go.mod b/examples/gosg-demo/go.mod deleted file mode 100644 index 4f816d36..00000000 --- a/examples/gosg-demo/go.mod +++ /dev/null @@ -1,28 +0,0 @@ -module github.com/bxcodec/dbresolver/v2/examples/gosg-demo - -go 1.20 - -require ( - github.com/Masterminds/squirrel v1.5.4 - github.com/bxcodec/dbresolver/v2 v2.0.0-00010101000000-000000000000 - github.com/labstack/echo/v4 v4.10.2 - github.com/lib/pq v1.10.9 -) - -require ( - github.com/labstack/gommon v0.4.0 // indirect - github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect - github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.17 // indirect - github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/valyala/fasttemplate v1.2.2 // indirect - go.uber.org/atomic v1.7.0 // indirect - go.uber.org/multierr v1.8.0 // indirect - golang.org/x/crypto v0.21.0 // indirect - golang.org/x/net v0.23.0 // indirect - golang.org/x/sys v0.18.0 // indirect - golang.org/x/text v0.14.0 // indirect -) - -replace github.com/bxcodec/dbresolver/v2 => ../.. diff --git a/examples/gosg-demo/go.sum b/examples/gosg-demo/go.sum deleted file mode 100644 index 97932a16..00000000 --- a/examples/gosg-demo/go.sum +++ /dev/null @@ -1,56 +0,0 @@ -github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= -github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= -github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= -github.com/labstack/echo/v4 v4.10.2 h1:n1jAhnq/elIFTHr1EYpiYtyKgx4RW9ccVgkqByZaN2M= -github.com/labstack/echo/v4 v4.10.2/go.mod h1:OEyqf2//K1DFdE57vw2DRgWY0M7s65IVQO2FzvI4J5k= -github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8= -github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= -github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= -github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= -github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= -github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= -github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= -github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= -github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= -github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= -github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= -github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= -go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= -go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8= -go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= -golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= -golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= -golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= -golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/examples/gosg-demo/internal/postgres/migrations/20230405142308_create_article_table.down.sql b/examples/gosg-demo/internal/postgres/migrations/20230405142308_create_article_table.down.sql deleted file mode 100644 index 7d16cb53..00000000 --- a/examples/gosg-demo/internal/postgres/migrations/20230405142308_create_article_table.down.sql +++ /dev/null @@ -1,5 +0,0 @@ -BEGIN; - -DROP TABLE IF EXISTS articles; - -END; \ No newline at end of file diff --git a/examples/gosg-demo/internal/postgres/migrations/20230405142308_create_article_table.up.sql b/examples/gosg-demo/internal/postgres/migrations/20230405142308_create_article_table.up.sql deleted file mode 100644 index c56689f9..00000000 --- a/examples/gosg-demo/internal/postgres/migrations/20230405142308_create_article_table.up.sql +++ /dev/null @@ -1,10 +0,0 @@ -BEGIN; - -CREATE TABLE IF NOT EXISTS articles ( - article_id serial PRIMARY KEY, - title VARCHAR ( 150 ) NOT NULL, - content TEXT NOT NULL, - created_time TIMESTAMP NOT NULL -); - -END; \ No newline at end of file diff --git a/examples/gosg-demo/main.go b/examples/gosg-demo/main.go deleted file mode 100644 index 8896322d..00000000 --- a/examples/gosg-demo/main.go +++ /dev/null @@ -1,246 +0,0 @@ -package main - -import ( - "context" - "database/sql" - "fmt" - "github.com/bxcodec/dbresolver/v2" - "github.com/labstack/echo/v4" - "log" - "net/http" - "strconv" - "time" - - "github.com/Masterminds/squirrel" - _ "github.com/lib/pq" -) - -func initDBResolver() dbresolver.DB { - var ( - rwHost = "localhost" - rwPort = 5432 - rwUser = "postgres" - rwPassword = "my_password" - roHost = "localhost" - roPort = 5433 - roUser = "postgres" - roPassword = "my_password" - dbname = "my_database" - ) - // connection string - rwPrimary := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", rwHost, rwPort, rwUser, rwPassword, dbname) - readOnlyReplica := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", roHost, roPort, roUser, roPassword, dbname) - - // open database for primary - dbPrimary, err := sql.Open("postgres", rwPrimary) - if err != nil { - log.Fatal("go error when connecting to the RW DB") - } - // configure the DBs for other setup eg, tracing, etc - // eg, tracing.Postgres(dbPrimary) - - // open database for replica - dbReadOnlyReplica, err := sql.Open("postgres", readOnlyReplica) - if err != nil { - log.Fatal("go error when connecting to the RO DB") - } - // configure the DBs for other setup eg, tracing, etc - // eg, tracing.Postgres(dbReadOnlyReplica) - - connectionDB := dbresolver.New( - dbresolver.WithPrimaryDBs(dbPrimary), - dbresolver.WithReplicaDBs(dbReadOnlyReplica), - dbresolver.WithLoadBalancer(dbresolver.RoundRobinLB)) - return connectionDB -} - -func main() { - connectionDB := initDBResolver() - defer connectionDB.Close() - // now you can use the connection for all DB operation - insertedIDs := insertMasterData(connectionDB) - res := queryArticles(connectionDB, insertedIDs) - fmt.Println("Queried Articles: ", res) - - stmt, err := connectionDB.Prepare("SELECT article_id, title, content FROM articles WHERE article_id = $1") - if err != nil { - log.Print("failed to prepare the query", err) - } - defer stmt.Close() - - e := echo.New() - e.GET("/", func(c echo.Context) error { - res := queryArticles(connectionDB, insertedIDs) - fmt.Println("Queried Articles: ", res) - - res = queryArticlesWithoutPrepare(connectionDB, insertedIDs) - fmt.Println("Queried Articles Without Prepare ", res) - - id := c.QueryParam("id") - idInt, err := strconv.ParseInt(id, 10, 64) - if id != "" && err != nil { - return c.String(http.StatusBadRequest, "invalid id") - } - if idInt > 0 { - singleArticle := queryRow(connectionDB, idInt) - fmt.Println("Queried Single Article: ", singleArticle) - - singleArticlePrepared := queryRowPrepare(connectionDB, idInt) - fmt.Println("Queried Single Article: ", singleArticlePrepared) - - singleArticlePreparedStmt := queryRowPreparedStmt(stmt, idInt) - fmt.Println("Queried Single Article with Prepared Stmt: ", singleArticlePreparedStmt) - } - - return c.String(http.StatusOK, "Hello, World!") - }) - e.Logger.Fatal(e.Start(":1323")) -} - -func insertMasterData(db dbresolver.DB) []int64 { - articles := []Article{ - { - Title: "Lorem Ipsum", - Content: "Dolor Sit Amet", - }, - } - - // we're using transaction here from app-level - // to tell the library to use RW connection - // disabling this will raise issue: " pq: cannot execute INSERT in a read-only transaction" - tx, errTx := db.Begin() - if errTx != nil { - log.Fatal("failed to begin ", errTx) - } - stmt, err := tx.PrepareContext(context.Background(), - "INSERT INTO articles (title, content, created_time) values ($1, $2, $3) RETURNING article_id;") - if err != nil { - log.Fatal("failed to insert master data ", err) - } - defer stmt.Close() - articleIds := []int64{} - for index, article := range articles { - - row := stmt.QueryRow(article.Title, article.Content, time.Now()) - var id int64 - err = row.Scan(&id) - if err != nil { - log.Println("failed to insert new article, ", err) - } - articleIds = append(articleIds, id) - articles[index].ID = id - } - tx.Commit() - fmt.Println("Inserted Articles ", articles) - return articleIds -} - -func queryArticles(db dbresolver.DB, articleIDs []int64) []Article { - sql, args, _ := squirrel.Select("article_id", "title", "content"). - From("articles").PlaceholderFormat(squirrel.Dollar). - Where(squirrel.Eq{"article_id": articleIDs}).ToSql() - stmt, err := db.Prepare(sql) - if err != nil { - log.Print("failed to prepare the query", err) - } - defer stmt.Close() - rows, err := stmt.Query(args...) - if err != nil { - log.Print("failed to query using IDs", err) - } - - res := []Article{} - for rows.Next() { - var article Article - var articleID int64 - errScan := rows.Scan(&articleID, &article.Title, &article.Content) - if errScan != nil { - log.Print("failed to scan rows 1, ", errScan) - } - - article.ID = articleID - res = append(res, article) - } - return res -} - -func queryRowPrepare(db dbresolver.DB, articleID int64) Article { - stmt, err := db.Prepare("SELECT article_id, title, content FROM articles WHERE article_id = $1") - if err != nil { - log.Print("failed to prepare the query", err) - } - defer stmt.Close() - row := stmt.QueryRow(articleID) - var article Article - var dbArticleID int64 - errScan := row.Scan(&dbArticleID, &article.Title, &article.Content) - if errScan != nil { - log.Print("failed to scan rows 2, ", errScan) - } - article.ID = dbArticleID - return article -} - -func queryRowPreparedStmt(stmt dbresolver.Stmt, articleID int64) Article { - row := stmt.QueryRow(articleID) - var article Article - var dbArticleID int64 - errScan := row.Scan(&articleID, &article.Title, &article.Content) - if errScan != nil { - log.Print("failed to scan rows 3, ", errScan) - } - article.ID = dbArticleID - return article -} - -func queryRow(db dbresolver.DB, articleID int64) Article { - sql, args, err := squirrel.Select("article_id", "title", "content"). - From("articles").PlaceholderFormat(squirrel.Dollar). - Where(squirrel.Eq{"article_id": articleID}).ToSql() - if err != nil { - log.Print("failed to build the query", err) - } - row := db.QueryRow(sql, args...) - - var article Article - var dbArticleID int64 - errScan := row.Scan(&dbArticleID, &article.Title, &article.Content) - if errScan != nil { - log.Print("failed to scan rows 4, ", errScan) - } - article.ID = dbArticleID - return article -} - -func queryArticlesWithoutPrepare(db dbresolver.DB, articleIDs []int64) []Article { - sql, args, err := squirrel.Select("article_id", "title", "content"). - From("articles").PlaceholderFormat(squirrel.Dollar). - Where(squirrel.Eq{"article_id": articleIDs}).ToSql() - if err != nil { - log.Print("failed to build the query", err) - } - rows, err := db.Query(sql, args...) - if err != nil { - log.Print("failed to run the query: ", sql, err) - } - res := []Article{} - for rows.Next() { - var article Article - var articleID int64 - errScan := rows.Scan(&articleID, &article.Title, &article.Content) - if errScan != nil { - log.Print("failed to scan rows 5, ", errScan) - } - - article.ID = articleID - res = append(res, article) - } - return res -} - -type Article struct { - ID int64 - Title string - Content string - CreatedTime string -} diff --git a/go.mod b/go.mod index 2aec1e03..aad16da6 100644 --- a/go.mod +++ b/go.mod @@ -1,18 +1,15 @@ module github.com/bxcodec/dbresolver/v2 -go 1.18 +go 1.22 require ( github.com/DATA-DOG/go-sqlmock v1.5.2 github.com/google/gofuzz v1.2.0 github.com/lib/pq v1.10.9 - go.uber.org/multierr v1.8.0 + go.uber.org/multierr v1.11.0 ) -require ( - github.com/stretchr/testify v1.8.1 // indirect - go.uber.org/atomic v1.7.0 // indirect -) +require github.com/stretchr/testify v1.8.1 // indirect retract ( // below versions doesn't support Update,Insert queries with "RETURNING CLAUSE" diff --git a/go.sum b/go.sum index 8eb9a900..ca21ca79 100644 --- a/go.sum +++ b/go.sum @@ -13,18 +13,13 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= -go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8= -go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/go.work b/go.work deleted file mode 100644 index 47291188..00000000 --- a/go.work +++ /dev/null @@ -1,6 +0,0 @@ -go 1.20 - -use ( - . - examples/gosg-demo -) diff --git a/go.work.sum b/go.work.sum deleted file mode 100644 index fde6c3e0..00000000 --- a/go.work.sum +++ /dev/null @@ -1,10 +0,0 @@ -github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= -github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= diff --git a/loadbalancer.go b/loadbalancer.go index 15a7e606..92744452 100644 --- a/loadbalancer.go +++ b/loadbalancer.go @@ -42,9 +42,9 @@ func (lb RandomLoadBalancer[T]) Resolve(dbs []T) T { } func (lb RandomLoadBalancer[T]) predict(n int) int { - rand.Seed(time.Now().UnixNano()) - max := n - 1 - min := 0 + rand.Seed(time.Now().UnixNano()) //nolint + max := n - 1 //nolint + min := 0 //nolint idx := rand.Intn(max-min+1) + min lb.randInt <- idx return idx @@ -62,21 +62,14 @@ func (lb RoundRobinLoadBalancer[T]) Name() LoadBalancerPolicy { // Resolve return the resolved option for RoundRobin LB func (lb *RoundRobinLoadBalancer[T]) Resolve(dbs []T) T { - idx := lb.roundRobin(len(dbs)) + idx := lb.predict(len(dbs)) return dbs[idx] } -func (lb *RoundRobinLoadBalancer[T]) roundRobin(n int) int { - if n <= 1 { - return 0 - } - return int(atomic.AddUint64(&lb.counter, 1) % uint64(n)) -} - func (lb *RoundRobinLoadBalancer[T]) predict(n int) int { if n <= 1 { return 0 } - counter := lb.counter - return int(atomic.AddUint64(&counter, 1) % uint64(n)) + // counter := lb.counter + return int(atomic.AddUint64(&lb.counter, 1) % uint64(n)) } diff --git a/loadbalancer_test.go b/loadbalancer_test.go index e0d777f1..8d9f7472 100644 --- a/loadbalancer_test.go +++ b/loadbalancer_test.go @@ -11,7 +11,7 @@ func TestReplicaRoundRobin(t *testing.T) { last := -1 err := quick.Check(func(n int) bool { - index := db.roundRobin(n) + index := db.predict(n) if n <= 1 { return index == 0 } diff --git a/misc/makefile/tools.Makefile b/misc/makefile/tools.Makefile index e78b39f5..e493522b 100644 --- a/misc/makefile/tools.Makefile +++ b/misc/makefile/tools.Makefile @@ -1,5 +1,11 @@ # This makefile should be used to hold functions/variables +ifeq ($(ARCH),x86_64) + ARCH := amd64 +else ifeq ($(ARCH),aarch64) + ARCH := arm64 +endif + define github_url https://github.com/$(GITHUB)/releases/download/v$(VERSION)/$(ARCHIVE) endef @@ -15,11 +21,12 @@ bin: GOTESTSUM := $(shell command -v gotestsum || echo "bin/gotestsum") gotestsum: bin/gotestsum ## Installs gotestsum (testing go code) -bin/gotestsum: VERSION := 1.8.1 +bin/gotestsum: VERSION := 1.12.0 bin/gotestsum: GITHUB := gotestyourself/gotestsum -bin/gotestsum: ARCHIVE := gotestsum_$(VERSION)_$(OSTYPE)_amd64.tar.gz +bin/gotestsum: ARCHIVE := gotestsum_$(VERSION)_$(OSTYPE)_$(ARCH).tar.gz bin/gotestsum: bin @ printf "Install gotestsum... " + @ printf "$(github_url)\n" @ curl -Ls $(shell echo $(call github_url) | tr A-Z a-z) | tar -zOxf - gotestsum > $@ && chmod +x $@ @ echo "done." @@ -28,12 +35,14 @@ bin/gotestsum: bin TPARSE := $(shell command -v tparse || echo "bin/tparse") tparse: bin/tparse ## Installs tparse (testing go code) -bin/tparse: VERSION := 0.11.1 +# eg https://github.com/mfridman/tparse/releases/download/v0.13.2/tparse_darwin_arm64 +export TPARSE_ARCH := $(shell uname -m) +bin/tparse: VERSION := 0.13.3 bin/tparse: GITHUB := mfridman/tparse -bin/tparse: ARCHIVE := tparse_$(OSTYPE)_x86_64 +bin/tparse: ARCHIVE := tparse_$(OSTYPE)_$(TPARSE_ARCH) #this is custom bin/tparse: bin @ printf "Install tparse... " - @ echo $(ARCHIVE) + @ printf "$(github_url)\n" @ curl -Ls $(call github_url) > $@ && chmod +x $@ @ echo "done." @@ -42,10 +51,11 @@ bin/tparse: bin GOLANGCI := $(shell command -v golangci-lint || echo "bin/golangci-lint") golangci-lint: bin/golangci-lint ## Installs golangci-lint (linter) -bin/golangci-lint: VERSION := 1.50.1 +bin/golangci-lint: VERSION := 1.59.0 bin/golangci-lint: GITHUB := golangci/golangci-lint -bin/golangci-lint: ARCHIVE := golangci-lint-$(VERSION)-$(OSTYPE)-amd64.tar.gz +bin/golangci-lint: ARCHIVE := golangci-lint-$(VERSION)-$(OSTYPE)-$(ARCH).tar.gz bin/golangci-lint: bin @ printf "Install golangci-linter... " - @ curl -Ls $(shell echo $(call github_url) | tr A-Z a-z) | tar -zOxf - $(shell printf golangci-lint-$(VERSION)-$(OSTYPE)-amd64/golangci-lint | tr A-Z a-z ) > $@ && chmod +x $@ + @ printf "$(github_url)\n" + @ curl -Ls $(shell echo $(call github_url) | tr A-Z a-z) | tar -zOxf - $(shell printf golangci-lint-$(VERSION)-$(OSTYPE)-$(ARCH)/golangci-lint | tr A-Z a-z ) > $@ && chmod +x $@ @ echo "done." \ No newline at end of file