#44 Gin 框架自学: 基础

2021-05-06

先列一下目前最火的几个 Go 语言框架:

  • gin
    stars num forks num License
    stars
  • beego 国人开发
    stars num forks num License
    stars
  • iris The fastest HTTP/2 Go Web Framework.
    自称是 ExpressJS 和 Laravel 的继承者。
    stars num forks num License
  • echo High performance, minimalist Go web framework
    stars num forks num License
  • go-micro Microservice framework for Go
    stars num forks num License
    stars
  • fiber
    stars num forks num License
  • kratos A Go framework for microservices. B 站出品
    stars num forks num License
  • revel/revel
    stars num forks num License
  • go-zero 国人开发(七牛)
    stars num forks num License
    stars
  • martini Classy web framework for Go
    stars num forks num License
  • GoFrame Classy web framework for Go 国人开发
    stars num forks num License

Gin

http://gin-gonic.com

Gin LOGO

Gin 相比 Beego 非常轻量级,不带 ORM,不支持 Session,正则路由都不支持,需要自行实现。

最新版本 56 个 文件, 算上注释,大概七千行代码。

$ find . -name "*.go" -not -path "*_test.go" | cat | wc -l
56
$ find . -name "*.go" -not -path "*_test.go" | xargs cat | wc -l
6910
Tag CreatedAt ReleasedAt
v1.9.0 2023-02-21 2023-02-21
v1.8.2 2022-12-22 2022-12-22
v1.8.1 2022-06-06 2022-06-06
v1.8.0 2022-05-30 2022-05-30
v1.7.7 2021-11-24 2021-11-24
v1.7.6 2021-08-03 2021-11-23
v1.7.4 2021-08-03 2021-08-15
v1.7.3 2021-08-03 2021-08-03
v1.7.2 2021-05-21 2021-05-21
v1.7.1 2021-04-08 2021-04-08
v1.7.0 2021-04-08 2021-04-08
v1.6.3 2020-05-03 2020-05-03
v1.6.2 2020-03-27 2020-03-27
v1.6.1 2020-03-23 2020-03-23
v1.6.0 2020-03-22 2020-03-22
v1.5.0 2019-11-24 2019-11-28
v1.4.0 2019-05-07 2019-05-08
v1.3.0 2018-08-14 2018-08-14
v1.2 2017-07-02 2017-07-02
v1.1.4 2016-12-04 2016-12-05
v1.1.3 2016-12-03 2016-12-04

HTTP 参数

// c: *gin.Context
// func (c *gin.Context) {
//     balabala...
// }

c.Request.URL.Query()

c.Param(key string) string // 路径参数
c.Params.ByName(key)

c.Query(key string) string // 查询参数
c.Request.URL.Query().Get(key)
c.GetQuery(key)

c.QueryMap(key string) map[string][]string
c.GetQueryMap(key)

c.QueryArray(key string) []string

c.GetQuery(key string) (string, bool)
c.GetQueryArray(key) 的第 0 个元素

c.GetQueryMap(key string) (map[string]string, bool)
c.GetQueryArray(key string) (values []string, ok bool)
c.PostForm(key string) string
c.PostFormMap(key string) map[string][]string
c.PostFormArray(key string) []string
c.GetPostForm(key string) (string, bool)
c.GetPostFormMap(key string) (map[string]string, bool)
c.GetPostFormArray(key string) (values []string, ok bool)
c.FormFile(key string) (*multipart.FileHeader, error)
c.DefaultQuery(key string, defaultValue string) string
c.DefaultPostForm(key string, defaultValue string) string

快速启动

package main

import (
    "io"
    "net/http"
    "os"
    "path/filepath"

    "github.com/gin-gonic/gin"
)

func main() {
    r := gin.Default()

    // 处理 GET 请求中的 URL 参数
    r.GET("/hello", func(c *gin.Context) {
        name := c.Query("name")
        c.String(http.StatusOK, "Hello %s!", name)
    })

    // 处理带路径参数的 GET 请求
    r.GET("/users/:id", func(c *gin.Context) {
        id := c.Param("id")
        c.String(http.StatusOK, "User ID: %s", id)
    })

    // 处理 POST 请求中的 JSON 请求体
    r.POST("/data", func(c *gin.Context) {
        var json struct {
            Data string `json:"data"`
        }
        c.BindJSON(&json)
        c.JSON(http.StatusOK, gin.H{"message": "Received data: " + json.Data})
    })

    // 处理文件上传
    r.POST("/upload", func(c *gin.Context) {
        file, err := c.FormFile("file")
        if err != nil {
            c.String(http.StatusBadRequest, "File upload failed: %s", err.Error())
            return
        }

        // 将上传的文件保存到服务器上
        dst := filepath.Join("./uploads", file.Filename)
        if err := c.SaveUploadedFile(file, dst); err != nil {
            c.String(http.StatusInternalServerError, "File save failed: %s", err.Error())
            return
        }

        c.String(http.StatusOK, "File uploaded successfully")
    })

    // 处理返回文件
    r.GET("/download", func(c *gin.Context) {
        filename := "example.txt"
        filepath := "./downloads/" + filename

        // 打开文件
        file, err := os.Open(filepath)
        if err != nil {
            c.String(http.StatusInternalServerError, "Failed to open file: %s", err.Error())
            return
        }
        defer file.Close()

        // 设置响应头
        c.Header("Content-Disposition", "attachment; filename="+filename)
        c.Header("Content-Type", "application/octet-stream")
        c.Header("Content-Transfer-Encoding", "binary")

        // 将文件内容写入响应主体
        _, err = io.Copy(c.Writer, file)
        if err != nil {
            c.String(http.StatusInternalServerError, "Failed to write file: %s", err.Error())
        }
    })

    r.Run(":8080")
}

路由

设置路由不存在的处理方法:

r.NoRoute(func(c *gin.Context) {
    c.JSON(404, gin.H{"message": "page not found"})
})

限制请求大小:

r.MaxMultipartMemory = 10 << 20 // 10MB
// "github.com/gin-contrib/size"
r.Use(size.Check(size.MB * 10))
r.Use(requestSizeLimitMiddleware(10 << 20))
func requestSizeLimitMiddleware(maxSize int64) gin.HandlerFunc {
    return func(c *gin.Context) {
        if c.Request.ContentLength > maxSize {
            c.AbortWithStatusJSON(413, gin.H{"message": "request entity too large"})
            return
        }
        c.Next()
    }
}

#43 Golang 自动过期 Map

2021-04-26
type ExpiringMap struct {
    m      sync.Map
    expiry time.Duration
}

func (e *ExpiringMap) set(key, value interface{}) {
    e.m.Store(key, value)

    go func() {
        time.Sleep(e.expiry)
        e.m.Delete(key)
    }()
}

改进:定时清理

package main

import (
    "sync"
    "time"
)

type Item struct {
    value      interface{}
    expiration int64
}

type ExpiringMap struct {
    m       sync.Mutex
    items   map[string]Item
    timeout time.Duration
}

func NewExpiringMap(timeout time.Duration) *ExpiringMap {
    em := &ExpiringMap{
        items:   make(map[string]Item),
        timeout: timeout,
    }
    go em.cleanup()
    return em
}

func (em *ExpiringMap) Set(key string, value interface{}) {
    em.m.Lock()
    defer em.m.Unlock()
    em.items[key] = Item{
        value:      value,
        expiration: time.Now().Add(em.timeout).UnixNano(),
    }
}

func (em *ExpiringMap) Get(key string) (interface{}, bool) {
    em.m.Lock()
    defer em.m.Unlock()
    item, found := em.items[key]
    if !found {
        return nil, false
    }
    if time.Now().UnixNano() > item.expiration {
        return nil, false
    }
    return item.value, true
}

func (em *ExpiringMap) Delete(key string) {
    em.m.Lock()
    defer em.m.Unlock()
    delete(em.items, key)
}

func (em *ExpiringMap) deleteExpiredKeys() {
    em.m.Lock()
    defer em.m.Unlock()
    now := time.Now().UnixNano()
    for key, item := range em.items {
        if now < item.expiration {
            break
        }
        delete(em.items, key)
    }
}

func (em *ExpiringMap) cleanup() {
    for {
        time.Sleep(em.timeout) // 阻塞协程
        em.deleteExpiredKeys()
    }
}

func main() {
    em := NewExpiringMap(time.Second * 10)

    time.Sleep(time.Second * 15)

    _, ok := em.Get("key1")
    if !ok {
        println("key1 is deleted")
    }

    _, ok = em.Get("key2")
    if !ok {
        println("key2 is deleted")
    }
}

#42 fasthttp: 十倍速

2021-04-26

https://github.com/valyala/fasthttp

fasthttp

package main

import (
    "flag"
    "fmt"
    "log"

    "github.com/valyala/fasthttp"
)

var (
    addr     = flag.String("addr", ":8080", "TCP address to listen to")
    compress = flag.Bool("compress", false, "Whether to enable transparent response compression")
)

func main() {
    flag.Parse()

    h := requestHandler
    if *compress {
        h = fasthttp.CompressHandler(h)
    }

    if err := fasthttp.ListenAndServe(*addr, h); err != nil {
        log.Fatalf("Error in ListenAndServe: %v", err)
    }
}

func requestHandler(ctx *fasthttp.RequestCtx) {
    fmt.Fprintf(ctx, "Hello, world!\n\n")

    fmt.Fprintf(ctx, "Request method is %q\n", ctx.Method())
    fmt.Fprintf(ctx, "RequestURI is %q\n", ctx.RequestURI())
    fmt.Fprintf(ctx, "Requested path is %q\n", ctx.Path())
    fmt.Fprintf(ctx, "Host is %q\n", ctx.Host())
    fmt.Fprintf(ctx, "Query string is %q\n", ctx.QueryArgs())
    fmt.Fprintf(ctx, "User-Agent is %q\n", ctx.UserAgent())
    fmt.Fprintf(ctx, "Connection has been established at %s\n", ctx.ConnTime())
    fmt.Fprintf(ctx, "Request has been started at %s\n", ctx.Time())
    fmt.Fprintf(ctx, "Serial request number for the current connection is %d\n", ctx.ConnRequestNum())
    fmt.Fprintf(ctx, "Your ip is %q\n\n", ctx.RemoteIP())

    fmt.Fprintf(ctx, "Raw request is:\n---CUT---\n%s\n---CUT---", &ctx.Request)

    ctx.SetContentType("text/plain; charset=utf8")

    // Set arbitrary headers
    ctx.Response.Header.Set("X-My-Header", "my-header-value")

    // Set cookies
    var c fasthttp.Cookie
    c.SetKey("cookie-name")
    c.SetValue("cookie-value")
    ctx.Response.Header.SetCookie(&c)
}
  • r.Body -> ctx.PostBody()
  • r.URL.Path -> ctx.Path()
  • r.URL -> ctx.URI()
  • r.Method -> ctx.Method()
  • r.Header -> ctx.Request.Header
  • r.Header.Get() -> ctx.Request.Header.Peek()
  • r.Host -> ctx.Host()
  • r.Form -> ctx.QueryArgs() + ctx.PostArgs()
  • r.PostForm -> ctx.PostArgs()
  • r.FormValue() -> ctx.FormValue()
  • r.FormFile() -> ctx.FormFile()
  • r.MultipartForm -> ctx.MultipartForm()
  • r.RemoteAddr -> ctx.RemoteAddr()
  • r.RequestURI -> ctx.RequestURI()
  • r.TLS -> ctx.IsTLS()
  • r.Cookie() -> ctx.Request.Header.Cookie()
  • r.Referer() -> ctx.Referer()
  • r.UserAgent() -> ctx.UserAgent()
  • w.Header() -> ctx.Response.Header
  • w.Header().Set() -> ctx.Response.Header.Set()
  • w.Header().Set("Content-Type") -> ctx.SetContentType()
  • w.Header().Set("Set-Cookie") -> ctx.Response.Header.SetCookie()
  • w.Write() -> ctx.Write(), ctx.SetBody(), ctx.SetBodyStream(), ctx.SetBodyStreamWriter()
  • w.WriteHeader() -> ctx.SetStatusCode()
  • w.(http.Hijacker).Hijack() -> ctx.Hijack()
  • http.Error() -> ctx.Error()
  • http.FileServer() -> fasthttp.FSHandler(), fasthttp.FS
  • http.ServeFile() -> fasthttp.ServeFile()
  • http.Redirect() -> ctx.Redirect()
  • http.NotFound() -> ctx.NotFound()
  • http.StripPrefix() -> fasthttp.PathRewriteFunc

fasthttp/router

  • /user/{user} named
  • /user/{user}_admin
  • /user/{user?} optional
  • /user/{name:[a-zA-Z]{5}} regex
  • /user/{name?:[a-zA-Z]{5}}
  • /user/{name:*} cache all

fasthttp/session

package main

import (
    "log"
    "time"

    "github.com/fasthttp/session/v2"
    "github.com/fasthttp/session/v2/providers/redis"
)

var serverSession *session.Session

func init() {
    var provider session.Provider
    var err error
    encoder := session.MSGPEncode
    decoder := session.MSGPDecode
    provider, err = redis.New(redis.Config{
        KeyPrefix:   "session",
        Addr:        "127.0.0.1:6379",
        PoolSize:    8,
        IdleTimeout: 30 * time.Second,
    })
    if err != nil {
        log.Fatal(err)
    }
    cfg := session.NewDefaultConfig()
    cfg.EncodeFunc = encoder
    cfg.DecodeFunc = decoder
    serverSession = session.New(cfg)
    if err = serverSession.SetProvider(provider); err != nil {
        log.Fatal(err)
    }
}
// 获取 SessionStore
store, err := serverSession.Get(ctx)
if err != nil {
    ctx.Error(err.Error(), fasthttp.StatusInternalServerError)
    return
}
defer func() {
    if err := serverSession.Save(ctx, store); err != nil {
        ctx.Error(err.Error(), fasthttp.StatusInternalServerError)
    }
}()

// Set
store.Set("foo", "bar")

// Get
val := store.Get("foo")
if val == nil {
    // ...
}

store.Delete(key)

store.Set(bytesKey)
val := store.GetBytes(bytesKey)
store.DeleteBytes(bytesKey)

data := store.GetAll()  // Dict
data := store.Ptr()     // *Dict

store.Flush()
store.Reset()

// Session ID,默认 sessionid
id := store.GetSessionID()
store.SetSessionID(bytesID)

// Session 有效期
changed := store.HasExpirationChanged()
duration := store.GetExpiration()
err := store.SetExpiration(duration)

savsgio/atreugo

构建与 fasthttp 和 fasthttp/router 之上的 HTTP 框架,做了一些简单的封装工作。
提供了几个示例

基础用法:

package main

import (
    "github.com/savsgio/atreugo/v11"
)

func main() {
    config := atreugo.Config{
        Addr: "0.0.0.0:8000",
    }
    server := atreugo.New(config)

    server.GET("/", func(ctx *atreugo.RequestCtx) error {
        return ctx.TextResponse("Hello World")
    })

    server.GET("/echo/{path:*}", func(ctx *atreugo.RequestCtx) error {
        return ctx.TextResponse("Echo message: " + ctx.UserValue("path").(string))
    })

    v1 := server.NewGroupPath("/v1")
    v1.GET("/", func(ctx *atreugo.RequestCtx) error {
        return ctx.TextResponse("Hello V1 Group")
    })

    if err := server.ListenAndServe(); err != nil {
        panic(err)
    }
}

#41 Golang: possible resource leak,'defer' is called in the 'for' loop

2021-04-01

类似代码:

for _, f := range files {
    fp, err := os.Open(f)
    if err != nil {
        panic(err)
    }
    defer fp.Close()
    // do something
}

Goland 提示:possible resource leak,'defer' is called in the 'for' loop

defer 是在最后函数退出时执行,但是变量已经经过循环覆盖,可能会导致内存泄漏。
似乎 Golang 的 defer 还是不够聪明。

换种写法:

for _, f := range files {
    func () {
        fp, err := os.Open(f)
        if err != nil {
            panic(err)
        }
        defer fp.Close()
        // do something
    }()
}

#40 Golang ORM 框架

2021-03-30
  • GORM
  • ent
  • Xorm 转到Gitea
  • Bun
  • Reform
  • GoRose

GitHub 搜索结果

  1. go-gorm/gorm shields.io:github/stars shields.io:github/languages/code-size shields.io:github/commit-activity/w shields.io:github/license
    The fantastic ORM library for Golang, aims to be developer friendly
  2. ent/ent shields.io:github/stars shields.io:github/languages/code-size shields.io:github/commit-activity/w shields.io:github/license
    An entity framework for Go
  3. geektutu/7days-golang shields.io:github/stars shields.io:github/languages/code-size shields.io:github/commit-activity/w shields.io:github/license
    7 days golang programs from scratch (web framework Gee, distributed cache GeeCache, object relational mapping ORM framework GeeORM, rpc framework GeeRPC etc) 7 天用 Go 动手写/从零实现系列
  4. gogf/gf shields.io:github/stars shields.io:github/languages/code-size shields.io:github/commit-activity/w shields.io:github/license
    GoFrame is a modular, powerful, high-performance and enterprise-class application development framework of Golang.
  5. sqlc-dev/sqlc shields.io:github/stars shields.io:github/languages/code-size shields.io:github/commit-activity/w shields.io:github/license
    Generate type-safe code from SQL
  6. go-xorm/xorm shields.io:github/stars shields.io:github/languages/code-size shields.io:github/commit-activity/w shields.io:github/license
    Simple and Powerful ORM for Go, support mysql,postgres,tidb,sqlite3,mssql,oracle, Moved to https://gitea.com/xorm/xorm
  7. volatiletech/sqlboiler shields.io:github/stars shields.io:github/languages/code-size shields.io:github/commit-activity/w shields.io:github/license
    Generate a Go ORM tailored to your database schema.
  8. go-pg/pg shields.io:github/stars shields.io:github/languages/code-size shields.io:github/commit-activity/w shields.io:github/license
    Golang ORM with focus on PostgreSQL features and performance
  9. go-gorp/gorp shields.io:github/stars shields.io:github/languages/code-size shields.io:github/commit-activity/w shields.io:github/license
    Go Relational Persistence - an ORM-ish library for Go
  10. xo/xo shields.io:github/stars shields.io:github/languages/code-size shields.io:github/commit-activity/w shields.io:github/license
    Command line tool to generate idiomatic Go code for SQL databases supporting PostgreSQL, MySQL, SQLite, Oracle, and Microsoft SQL Server
  11. upper/db shields.io:github/stars shields.io:github/languages/code-size shields.io:github/commit-activity/w shields.io:github/license
    Data access layer for PostgreSQL, CockroachDB, MySQL, SQLite and MongoDB with ORM-like features.
  12. uptrace/bun shields.io:github/stars shields.io:github/languages/code-size shields.io:github/commit-activity/w shields.io:github/license
    SQL-first Golang ORM
  13. xxjwxc/gormt shields.io:github/stars shields.io:github/languages/code-size shields.io:github/commit-activity/w shields.io:github/license
    database to golang struct
  14. steebchen/prisma-client-go shields.io:github/stars shields.io:github/languages/code-size shields.io:github/commit-activity/w shields.io:github/license
    Prisma Client Go is an auto-generated and fully type-safe database client
  15. xormplus/xorm shields.io:github/stars shields.io:github/languages/code-size shields.io:github/commit-activity/w shields.io:github/license
    xorm 是一个简单而强大的 Go 语言 ORM 库,通过它可以使数据库操作非常简便。本库是基于原版 xorm 的定制增强版本,为 xorm 提供类似 ibatis 的配置文件及动态 SQL 支持,支持 AcitveRecord 操作
  16. go-reform/reform shields.io:github/stars shields.io:github/languages/code-size shields.io:github/commit-activity/w shields.io:github/license
    A better ORM for Go, based on non-empty interfaces and code generation.
  17. gobuffalo/pop shields.io:github/stars shields.io:github/languages/code-size shields.io:github/commit-activity/w shields.io:github/license
    A Tasty Treat For All Your Database Needs
  18. bobohume/gonet shields.io:github/stars shields.io:github/languages/code-size shields.io:github/commit-activity/w shields.io:github/license
    go 分布式服务器,基于内存 mmo
  19. unionj-cloud/go-doudou shields.io:github/stars shields.io:github/languages/code-size shields.io:github/commit-activity/w shields.io:github/license
    go-doudou(doudou pronounce /dəudəu/)is OpenAPI 3.0 (for REST) spec and Protobuf v3 (for grpc) based lightweight microservice framework. It supports monolith service application as well.
  20. gohouse/gorose shields.io:github/stars shields.io:github/languages/code-size shields.io:github/commit-activity/w shields.io:github/license
    GoRose(go orm), a mini database ORM for golang, which inspired by the famous php framwork laravle's eloquent. It will be friendly for php developer and python or ruby developer. Currently provides six major database drivers: mysql,sqlite3,postgres,oracle,mssql, Clickhouse.
  21. huandu/go-sqlbuilder shields.io:github/stars shields.io:github/languages/code-size shields.io:github/commit-activity/w shields.io:github/license
    A flexible and powerful SQL string builder library plus a zero-config ORM.

#39 Golang 入门项目 httpbin 总结

2021-03-29

httpbin 是我练手的一个非常简单的小项目,功能就是:
1. HTTP POST 请求 (POST /) 提交一个字符串,服务器返回一个 ID。
1. HTTP GET 请求 (GET /xxxx),返回 ID 对应的字符串。

#38 Golang compress

2021-03-01
  • archive: 打包 (tar, zip)
    参考:《Golang archive
  • compress: 压缩 (bzip2, flate, gzip, lzw, zlib)

压缩 & 解压缩

import (
    "bytes"
    "compress/gzip"
    "io"
)

func compressData(data string) ([]byte, error) {
    var buf bytes.Buffer
    gz := gzip.NewWriter(&buf)
    _, err := gz.Write([]byte(data))
    if err != nil {
        return nil, err
    }
    if err := gz.Close(); err != nil {
        return nil, err
    }
    return buf.Bytes(), nil
}

func decompressData(compressedData []byte) (string, error) {
    r, err := gzip.NewReader(bytes.NewReader(compressedData))
    if err != nil {
        return "", err
    }
    defer r.Close()
    decompressedData, err := io.ReadAll(r)
    if err != nil {
        return "", err
    }
    return string(decompressedData), nil
}

压缩 & 解压缩 & 列出压缩包中的内容

import (
    "archive/zip"
)

// 压缩文件
func compressFile(zipFilename string, filenames ...string) error {
    zipFile, err := os.Create(zipFilename)
    if err != nil {
        return err
    }
    defer zipFile.Close()

    zipWriter := zip.NewWriter(zipFile)
    defer zipWriter.Close()

    for _, filename := range filenames {
        file, err := os.Open(filename)
        if err != nil {
            return err
        }
        defer file.Close()
        fi, err := file.Stat()
        if err != nil {
            return err
        }
        fileHeader, err := zip.FileInfoHeader(fi)
        if err != nil {
            return err
        }

        // 设置文件名(压缩后的文件名)
        fileHeader.Name = filename

        // 写入文件头部
        writer, err := zipWriter.CreateHeader(fileHeader)
        if err != nil {
            return err
        }

        // 复制文件内容到压缩包中
        _, err = io.Copy(writer, file)
        if err != nil {
            return err
        }
    }

    return nil
}

// 解压缩文件
func decompressFile(zipFilename, targetDir string) error {
    zipReader, err := zip.OpenReader(zipFilename)
    if err != nil {
        return err
    }
    defer zipReader.Close()

    for _, file := range zipReader.File {
        // 打开压缩包中的文件
        fileReader, err := file.Open()
        if err != nil {
            return err
        }
        defer fileReader.Close()

        // 创建目标文件
        extractedFilePath := fmt.Sprintf("%s/%s", targetDir, file.Name)
        extractedFile, err := os.Create(extractedFilePath)
        if err != nil {
            return err
        }
        defer extractedFile.Close()

        // 复制文件内容到目标文件
        _, err = io.Copy(extractedFile, fileReader)
        if err != nil {
            return err
        }
    }

    return nil
}

func listFilesInZip(zipFilename string) ([]string, error) {
    files := []string{}

    zipReader, err := zip.OpenReader(zipFilename)
    if err != nil {
        return files, err
    }
    defer zipReader.Close()

    for _, file := range zipReader.File {
        files = append(files, file.Name)
    }
    return files, nil
}

#37 Golang archive

2021-03-01

标准库中的 archive 支持 tarzip 两种打包格式。

https://en.wikipedia.org/wiki/Tar_(computing)

最常用的 tar 包:

func createTarArchive(sourceDir, targetFile string) error {
    file, err := os.Create(targetFile)
    if err != nil {
        return err
    }
    defer file.Close()

    tarWriter := tar.NewWriter(file)
    defer tarWriter.Close()

    return filepath.Walk(sourceDir, func(path string, info os.FileInfo, err error) error {
        if err != nil {
            return err
        }

        header, err := tar.FileInfoHeader(info, info.Name())
        if err != nil {
            return err
        }

        if err := tarWriter.WriteHeader(header); err != nil {
            return err
        }

        if !info.IsDir() {
            file, err := os.Open(path)
            if err != nil {
                return err
            }
            defer file.Close()

            _, err = io.Copy(tarWriter, file)
            if err != nil {
                return err
            }
        }

        return nil
    })
}

func extractTarArchive(sourceFile, targetDir string) error {
    file, err := os.Open(sourceFile)
    if err != nil {
        return err
    }
    defer file.Close()

    tarReader := tar.NewReader(file)
    for {
        header, err := tarReader.Next()
        if err == io.EOF {
            break
        }
        if err != nil {
            return err
        }
        targetPath := filepath.Join(targetDir, header.Name)
        if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil {
            return err
        }
        if header.Typeflag == tar.TypeReg {
            file, err := os.Create(targetPath)
            if err != nil {
                return err
            }
            defer file.Close()
            _, err = io.Copy(file, tarReader)
            if err != nil {
                return err
            }
        }
    }

    return nil
}