最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Golang調(diào)用FFmpeg實(shí)現(xiàn)視頻截圖,裁剪與水印添加功能

 更新時(shí)間:2026年02月10日 09:15:37   作者:冷冷的菜哥  
這篇文章主要為大家詳細(xì)介紹了Golang如何調(diào)用FFmpeg實(shí)現(xiàn)視頻截圖,裁剪與水印添加功能,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以了解下

1.視頻處理參數(shù)

package request

import (
	"mime/multipart"
)

// VideoSnapshot 視頻截圖
type VideoSnapshot struct {
	Video  *multipart.FileHeader `form:"video"`  // 視頻文件
	Second int64                 `form:"second"` // 截圖間隔秒
}

// VideoCut 視頻剪輯
type VideoCut struct {
	Video    *multipart.FileHeader `form:"video"`    // 視頻文件
	Start    int64                 `form:"start"`    // 開始時(shí)間(秒)
	Duration int64                 `form:"duration"` // 截止時(shí)間(秒)
}

// VideoWatermark 視頻水印
type VideoWatermark struct {
	Video     *multipart.FileHeader `form:"video"`     // 視頻文件
	Watermark *multipart.FileHeader `form:"watermark"` // 水印文件,必須是png
}

2.視頻處理接口及實(shí)現(xiàn)

package service

import (
	"context"
	"ry-go/common/request"

	"github.com/labstack/echo/v4"
)

// VideoProcessService 視頻處理接口
type VideoProcessService interface {
	Snapshot(e context.Context, param *request.VideoSnapshot) (string, error)
	Cut(e context.Context, param *request.VideoCut) (string, error)
	Watermark(e context.Context, param *request.VideoWatermark) (string, error)
}
package serviceImpl

import (
	"archive/zip"
	"bytes"
	"context"
	"fmt"
	"io"
	"mime/multipart"
	"net/http"
	"net/url"
	"os"
	"os/exec"
	"path/filepath"
	"ry-go/common/request"
	"ry-go/utils"
	"strconv"
	"strings"
	"time"

	"github.com/labstack/echo/v4"
	"github.com/rs/zerolog"
)

type VideoProcessServiceImpl struct {
}

func NewVideoProcessServiceImpl() *VideoProcessServiceImpl {
	return &VideoProcessServiceImpl{}
}

// saveUploadedFile 保存上傳的文件到臨時(shí)位置并返回路徑
func saveUploadedFile(fileHeader *multipart.FileHeader) (string, error) {
	logger := zerolog.DefaultContextLogger
	src, err := fileHeader.Open()
	if err != nil {
		logger.Error().Err(err).Msg("打開上傳文件失敗")
		return "", err
	}
	defer src.Close()

	tmpFile, err := os.CreateTemp("", "upload_*"+filepath.Ext(fileHeader.Filename))
	if err != nil {
		logger.Error().Err(err).Msgf("創(chuàng)建臨時(shí)文件==%v失敗", tmpFile)
		return "", err
	}
	defer tmpFile.Close()

	_, err = io.Copy(tmpFile, src)
	if err != nil {
		os.Remove(tmpFile.Name())
		return "", err
	}

	return tmpFile.Name(), nil
}

// getVideoDuration 返回視頻時(shí)長(zhǎng)(秒)
func getVideoDuration(ctx context.Context, videoPath string) (float64, error) {
	cmd := exec.CommandContext(ctx, "ffprobe",
		"-v", "error",
		"-show_entries", "format=duration",
		"-of", "default=noprint_wrappers=1:nokey=1",
		videoPath,
	)

	output, err := cmd.Output()
	if err != nil {
		return 0, fmt.Errorf("ffprobe failed: %w", err)
	}

	durationStr := strings.TrimSpace(string(output))
	if durationStr == "N/A" {
		return 0, fmt.Errorf("video duration is unavailable")
	}

	duration, err := strconv.ParseFloat(durationStr, 64)
	if err != nil {
		return 0, fmt.Errorf("invalid duration: %s", durationStr)
	}

	return duration, nil
}

func getVideoResolution(ctx context.Context, videoPath string) (width, height int, err error) {
	cmd := exec.CommandContext(
		ctx,
		"ffprobe",
		"-v", "error",
		"-select_streams", "v:0",
		"-show_entries", "stream=width,height",
		"-of", "csv=s=,:p=0",
		videoPath,
	)

	out, err := cmd.Output()
	if err != nil {
		return 0, 0, fmt.Errorf("ffprobe failed: %w", err)
	}

	line := strings.TrimSpace(string(out))
	if line == "" {
		return 0, 0, fmt.Errorf("empty ffprobe output")
	}

	parts := strings.Split(line, ",")
	if len(parts) != 2 {
		return 0, 0, fmt.Errorf("invalid resolution format: %s", line)
	}

	width, err = strconv.Atoi(strings.TrimSpace(parts[0]))
	if err != nil {
		return 0, 0, fmt.Errorf("invalid width: %w", err)
	}

	height, err = strconv.Atoi(strings.TrimSpace(parts[1]))
	if err != nil {
		return 0, 0, fmt.Errorf("invalid height: %w", err)
	}

	return width, height, nil
}

// runFfmpeg 執(zhí)行ffmpeg命令
func runFfmpeg(ctx context.Context, args ...string) error {
	logger := zerolog.DefaultContextLogger
	cmd := exec.CommandContext(ctx, "ffmpeg", args...)
	var stderr bytes.Buffer
	cmd.Stderr = &stderr

	if err := cmd.Run(); err != nil {
		if ctx.Err() != nil {
			return fmt.Errorf("operation cancelled: %w", ctx.Err())
		}
		logger.Error().Err(err).Msg("執(zhí)行ffmpeg失敗")
		return fmt.Errorf("ffmpeg failed: %w; stderr: %s", err, stderr.String())
	}
	return nil
}


func readDirFileCount(dir, suffix string) int64 {
	entries, _ := os.ReadDir(dir)
	count := int64(0)
	for _, e := range entries {
		if strings.HasSuffix(e.Name(), suffix) {
			count++
		}
	}

	return count
}


func zipDir(zipPath, dir string) error {
	zipFile, err := os.Create(zipPath)
	if err != nil {
		return err
	}
	defer zipFile.Close()

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

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

		relPath, err := filepath.Rel(dir, path)
		if err != nil {
			return err
		}

		fw, err := zipWriter.Create(relPath)
		if err != nil {
			return err
		}

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

		_, err = io.Copy(fw, fs)
		return err
	})
}

func (s *VideoProcessServiceImpl) Snapshot(ctx context.Context, param *request.VideoSnapshot) (string, error) {
	logger := zerolog.DefaultContextLogger
	if param.Video == nil {
		return "", fmt.Errorf("video file is required")
	}
	if param.Second < 1 {
		return "", fmt.Errorf("second must be >= 0")
	}

	videoPath, err := saveUploadedFile(param.Video)
	if err != nil {
		logger.Error().Err(err).Msgf("failed to save video %s", videoPath)
		return "", fmt.Errorf("failed to save video: %w", err)
	}
	defer func(name string) {
		if err = os.Remove(name); err != nil {
			logger.Error().Err(err).Msgf("failed to delete video %s", videoPath)
		}
		logger.Debug().Msgf("successful to delete video %s", videoPath)
	}(videoPath)


	// 創(chuàng)建臨時(shí)目錄存放所有截圖
	tempDir, err := os.MkdirTemp("", "snapshots_*")
	if err != nil {
		return "", fmt.Errorf("failed to create temp dir: %w", err)
	}

	logger.Debug().Msgf("temp dir === %s", tempDir)

	outputPattern := filepath.Join(tempDir, "snapshot_%04d.jpg")

	args := []string{
		"-i", videoPath,
		"-vf", fmt.Sprintf("fps=1/%d", param.Second),
		"-q:v", "2",
		"-y",
		outputPattern,
	}

	if err = runFfmpeg(ctx, args...); err != nil {
		os.RemoveAll(tempDir)
		logger.Error().Err(err).Msg("ffmpeg batch snapshot failed")
		return "", fmt.Errorf("batch snapshot failed: %w", err)
	}

	// 掃描目錄
	count := readDirFileCount(tempDir, ".jpg")

	if err = zipDir(zipPath, tempDir); err != nil {
		os.RemoveAll(tempDir)
		return "", fmt.Errorf("failed to create zip: %w", err)
	}

	// 清理原始圖片(保留 zip)
	os.RemoveAll(tempDir)
	logger.Debug().Msgf("successful to delete video temp %s", tempDir)
	logger.Debug().Msgf("Snapshot saved to path %s,count===%d", zipPath, count)
	return zipPath, nil
}

func (s *VideoProcessServiceImpl) Cut(ctx context.Context, param *request.VideoCut) (string, error) {
	logger := zerolog.DefaultContextLogger
	if param.Video == nil {
		logger.Error().Msg("video file can not null")
		return "", fmt.Errorf("video file is required")
	}

	videoPath, err := saveUploadedFile(param.Video)
	if err != nil {
		logger.Error().Err(err).Msgf("failed to save video %s", videoPath)
		return "", fmt.Errorf("failed to save video: %w", err)
	}
	defer os.Remove(videoPath)

	// 記錄開始時(shí)間
	startCut := time.Now()

	// 獲取視頻總時(shí)長(zhǎng)
	videoDuration, err := getVideoDuration(ctx, videoPath)
	if err != nil {
		return "", err
	}

	// 起始時(shí)間(秒)
	start := float64(param.Start)

	// 期望截取時(shí)長(zhǎng)(秒)
	wantDuration := float64(param.Duration)

	// 剩余可截取的最大時(shí)長(zhǎng)
	maxDuration := videoDuration - start
	if maxDuration <= 0 {
		return "", fmt.Errorf("start time exceeds video duration")
	}

	// 實(shí)際截取時(shí)長(zhǎng)
	actualDuration := wantDuration
	if wantDuration > maxDuration {
		actualDuration = maxDuration
	}
	
	startStr := fmt.Sprintf("%.3f", start)
	durationStr := fmt.Sprintf("%.3f", actualDuration)

	outputPath := filepath.Join(os.TempDir(), fmt.Sprintf("cut_%d.mp4", time.Now().Unix()))

	args := []string{
		"-i", videoPath,
		"-ss", startStr,
		"-to", durationStr,
		"-c", "copy",
		"-y",
		outputPath,
	}

	err = runFfmpeg(ctx, args...)
	if err != nil {
		return "", err
	}

	elapsed := time.Since(startCut)
	logger.Debug().Msgf("視頻截取耗時(shí)===%f秒", elapsed.Seconds())
	logger.Debug().Msgf("Cut video saved to: %s", outputPath)
	return outputPath, nil
}

func (s *VideoProcessServiceImpl) Watermark(ctx context.Context, param *request.VideoWatermark) (string, error) {
	logger := zerolog.DefaultContextLogger
	if len(param.Video.Header) == 0 {
		return "", fmt.Errorf("missing video file")
	}
	if len(param.Watermark.Header) == 0 {
		return "", fmt.Errorf("missing watermark image")
	}

	videoPath, err := saveUploadedFile(param.Video)
	if err != nil {
		logger.Error().Err(err).Msgf("failed to save video %s", videoPath)
		return "", fmt.Errorf("failed to save video: %w", err)
	}
	defer os.Remove(videoPath)

	watermarkPath, err := saveUploadedFile(param.Watermark)
	if err != nil {
		logger.Error().Err(err).Msgf("failed to save watermark %s", watermarkPath)
		return "", fmt.Errorf("failed to save watermark: %w", err)
	}
	defer os.Remove(watermarkPath)

	outputPath := filepath.Join(os.TempDir(), fmt.Sprintf("watermarked_%d.mp4", time.Now().Unix()))

	// 記錄開始時(shí)間
	start := time.Now()

	// 獲取視頻分辨率
	videoWidth, _, err := getVideoResolution(ctx, videoPath)
	if err != nil {
		return "", err
	}

	// 計(jì)算水印目標(biāo)寬度(15%,最小 100,最大 400)
	wmTargetWidth := int(float64(videoWidth) * 0.15)
	if wmTargetWidth < 100 {
		wmTargetWidth = 100
	}
	if wmTargetWidth > 400 {
		wmTargetWidth = 400
	}

	filter := fmt.Sprintf(
		"[1:v]scale=%d:-1[wm];[0:v][wm]overlay=main_w-overlay_w-10:10",
		wmTargetWidth,
	)

	err = runFfmpeg(
		ctx,
		"-i", videoPath,
		"-i", watermarkPath,
		"-filter_complex", filter,
		"-c:v", "libx264",
		"-c:a", "aac",
		"-strict", "-2",
		"-y",
		outputPath,
	)
	if err != nil {
		return "", err
	}

	elapsed := time.Since(start)
	logger.Debug().Msgf("視頻水印耗時(shí)===%f秒", elapsed.Seconds())
	logger.Debug().Msgf("Watermarked video saved to: %s", outputPath)
	return outputPath, nil
}

3.視頻處理控制器

package controller

import (
	"fmt"
	"net/http"
	"net/url"
	"os"
	"ry-go/business/service"
	"ry-go/business/service/serviceImpl"
	"ry-go/common/request"
	"ry-go/common/response"
	"ry-go/utils"
	"strings"
	"time"

	"github.com/labstack/echo/v4"
	"github.com/rs/zerolog"
	"github.com/spf13/cast"
)

type VideoProcessController struct {
	Service service.VideoProcessService
}

// NewVideoProcessController 控制器初始化
func NewVideoProcessController(s *serviceImpl.VideoProcessServiceImpl) *VideoProcessController {
	return &VideoProcessController{
		Service: s,
	}
}

func GetEchoForm(c echo.Context, maxMemory int64) (*multipart.Form, error) {
	// 解析表單,設(shè)置表單內(nèi)存緩存大小
	if err := c.Request().ParseMultipartForm(maxMemory); err != nil {
		return nil, err
	}
	return c.Request().MultipartForm, nil
}

// HandlerSnapshot 視頻截圖
func (c *VideoProcessController) HandlerSnapshot(e echo.Context) error {
	logger := zerolog.DefaultContextLogger

	form, err := GetEchoForm(e, 32<<20)
	if err != nil {
		response.NewRespCodeErr(e, http.StatusInternalServerError, err)
		return err
	}

	files := form.File["video"]
	if len(files) == 0 {
		response.NewRespCodeMsg(e, http.StatusBadRequest, "video file is required")
		return err
	}

	var second int64
	if vals := form.Value["start"]; len(vals) > 0 {
		second = cast.ToInt64(vals[0])
		if second < 1 {
			response.NewRespCodeMsg(e, http.StatusBadRequest, "start must be >= 1")
			return err
		}
	} else {
		response.NewRespCodeMsg(e, http.StatusBadRequest, "start is required")
		return err
	}

	zipPath, err := c.Service.Snapshot(e.Request().Context(), &request.VideoSnapshot{
		Video:  files[0],
		Second: second,
	})
	if err != nil {
		return err
	}

	replace := strings.Replace(time.Now().Local().Format("20060102150405.000"), ".", "", 1)
	uniqueFileName := url.PathEscape(fmt.Sprintf("snapshot_%s_%s.zip", utils.ShortUUID(), replace))
	e.Response().Header().Set(echo.HeaderContentDisposition,
		fmt.Sprintf("attachment; filename=\"%s\"; filename*=UTF-8''%s", uniqueFileName, uniqueFileName))

	if err = e.Attachment(zipPath, uniqueFileName); err != nil {
		// 發(fā)送失敗記錄日志
		fmt.Printf("Failed to send file %s: %v\n", zipPath, err)
		os.Remove(zipPath)
		response.NewRespCodeMsg(e, http.StatusInternalServerError, "failed to send result")
		return err
	}

	go func(path string) {
		time.Sleep(5 * time.Second)
		os.Remove(path)
		logger.Debug().Msgf("刪除了zip臨時(shí)文件路徑===%s", zipPath)
	}(zipPath)

	return nil
}

// HandlerCut 視頻截取
func (c *VideoProcessController) HandlerCut(e echo.Context) error {
	// 解析表單,設(shè)置緩存為 32MB
	form, err := GetEchoForm(e, 32<<20)
	if err != nil {
		response.NewRespCodeErr(e, http.StatusInternalServerError, err)
		return err
	}

	files := form.File["video"]
	if len(files) == 0 {
		response.NewRespCodeMsg(e, http.StatusBadRequest, "video file is required")
		return err
	}

	var start, duration int64
	if vals := form.Value["start"]; len(vals) > 0 {
		start = cast.ToInt64(vals[0])
	} else {
		response.NewRespCodeMsg(e, http.StatusBadRequest, "start is required")
		return err
	}

	if vals := form.Value["duration"]; len(vals) > 0 {
		duration = cast.ToInt64(vals[0])
		if duration <= 0 {
			response.NewRespCodeMsg(e, http.StatusBadRequest, "duration must be > 0")
			return err
		}

		if duration > 0 && duration <= start {
			response.NewRespCodeMsg(e, http.StatusBadRequest, "duration must be > 0 and muse be > start")
			return err
		}
	} else {
		response.NewRespCodeMsg(e, http.StatusBadRequest, "duration is required")
		return err
	}
	outputPath, err := c.Service.Cut(e.Request().Context(), &request.VideoCut{
		Video:    files[0],
		Start:    start,
		Duration: duration,
	})
	if err != nil {
		return err
	}
	// 生成唯一文件名
	replace := strings.Replace(time.Now().Local().Format("20060102150405.000"), ".", "", 1)
	uniqueFileName := url.PathEscape(fmt.Sprintf("cut_%s_%s.mp4", utils.ShortUUID(), replace))
	e.Response().Header().Set(echo.HeaderContentDisposition,
		fmt.Sprintf("attachment; filename=\"%s\"; filename*=UTF-8''%s", uniqueFileName, uniqueFileName))

	if err = e.Attachment(outputPath, uniqueFileName); err != nil {
		// 發(fā)送失敗記錄日志
		fmt.Printf("Failed to send file %s: %v\n", outputPath, err)
		os.Remove(outputPath)
		response.NewRespCodeMsg(e, http.StatusInternalServerError, "failed to send result")
		return err
	}

	go func(path string) {
		time.Sleep(5 * time.Second)
		os.Remove(path)
		zerolog.DefaultContextLogger.Debug().Msgf("刪除了視頻文件路徑===%s", outputPath)
	}(outputPath)

	return nil
}

// HandlerWatermark 視頻添加水印
func (c *VideoProcessController) HandlerWatermark(e echo.Context) error {
	// 解析表單,設(shè)置緩存為 32MB
	form, err := GetEchoForm(e, 32<<20)
	if err != nil {
		response.NewRespCodeErr(e, http.StatusInternalServerError, err)
		return err
	}

	outputPath, err := c.Service.Watermark(e.Request().Context(), &request.VideoWatermark{
		Video:     form.File["video"][0],
		Watermark: form.File["watermark"][0],
	})
	if err != nil {
		response.NewRespCodeErr(e, http.StatusInternalServerError, err)
		return err
	}

	replace := strings.Replace(time.Now().Local().Format("20060102150405.000"), ".", "", 1)
	uniqueFileName := url.PathEscape(fmt.Sprintf("%s_%s.mp4", utils.ShortUUID(), replace))
	e.Response().Header().Set(echo.HeaderContentDisposition,
		fmt.Sprintf("attachment; filename=\"%s\"; filename*=UTF-8''%s", uniqueFileName, uniqueFileName))

	if err = e.Attachment(outputPath, uniqueFileName); err != nil {
		// 發(fā)送失敗記錄日志
		fmt.Printf("Failed to send file %s: %v\n", outputPath, err)
		os.Remove(outputPath)
		response.NewRespCodeMsg(e, http.StatusInternalServerError, "failed to send result")
		return err
	}

	go func(path string) {
		time.Sleep(10 * time.Second)
		os.Remove(path)
		zerolog.DefaultContextLogger.Debug().Msgf("刪除了視頻文件路徑===%s", outputPath)
	}(outputPath)

	return nil
}

4.視頻處理路由配置

package routers

import (
	"ry-go/business/controller"

	"github.com/labstack/echo/v4"
)

func VideoProcessRouterInit(group *echo.Group, videoController *controller.VideoProcessController) {
	routerGroup := group.Group("/video")
	routerGroup.POST("/snapshot", videoController.HandlerSnapshot)
	routerGroup.POST("/cut", videoController.HandlerCut)
	routerGroup.POST("/watermark", videoController.HandlerWatermark)
}

這里的路由使用了echo,具體用法這里不做詳細(xì)說(shuō)明了,具體內(nèi)容請(qǐng)自行查看官網(wǎng)文檔

5.其他說(shuō)明

注意:上述代碼依賴 ffmpeg 工具,請(qǐng)先確保它已在您的系統(tǒng)中安裝并配置好環(huán)境變量。如果您尚未安裝,可參考官方文檔或相關(guān)教程完成安裝,本文不再詳細(xì)介紹。詳情參見ffmpeg官網(wǎng)

上述代碼均使用apifox測(cè)試,功能正常

到此這篇關(guān)于Golang調(diào)用FFmpeg實(shí)現(xiàn)視頻截圖,裁剪與水印添加功能的文章就介紹到這了,更多相關(guān)Go FFmpeg視頻處理內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • 一文帶你使用golang手?jǐn)]一個(gè)websocket中間件

    一文帶你使用golang手?jǐn)]一個(gè)websocket中間件

    這篇文章主要為大家詳細(xì)介紹了如何使用golang手?jǐn)]一個(gè)websocket中間件,文中的示例代碼講解詳細(xì),具有一定的借鑒價(jià)值,感興趣的小伙伴可以參考一下
    2023-12-12
  • Go panic和recover函數(shù)使用細(xì)節(jié)深入探究

    Go panic和recover函數(shù)使用細(xì)節(jié)深入探究

    這篇文章主要為大家介紹了Go?的panic和recover函數(shù)使用細(xì)節(jié)深入探究,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-12-12
  • Golang使用RPC實(shí)現(xiàn)程序遠(yuǎn)程調(diào)用詳解

    Golang使用RPC實(shí)現(xiàn)程序遠(yuǎn)程調(diào)用詳解

    RPC 讓你在寫代碼時(shí),調(diào)用另一臺(tái)遠(yuǎn)程機(jī)器上的函數(shù),就像調(diào)用本地的函數(shù)一樣簡(jiǎn)單,下面小編就和大家詳細(xì)介紹一下Golang如何使用RPC實(shí)現(xiàn)程序遠(yuǎn)程調(diào)用吧
    2026-03-03
  • gRPC超時(shí)攔截器實(shí)現(xiàn)示例

    gRPC超時(shí)攔截器實(shí)現(xiàn)示例

    這篇文章主要為大家介紹了gRPC超時(shí)攔截器實(shí)現(xiàn)示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-06-06
  • go select的用法

    go select的用法

    本文主要介紹了go select的用法,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2022-01-01
  • Go使用SM3哈希算法和隨機(jī)鹽實(shí)現(xiàn)用戶密碼加密

    Go使用SM3哈希算法和隨機(jī)鹽實(shí)現(xiàn)用戶密碼加密

    在現(xiàn)代 Web 系統(tǒng)中,用戶密碼安全存儲(chǔ)是非常關(guān)鍵的一環(huán),本文將介紹如何使用 SM3 哈希算法 + 隨機(jī)鹽 (salt) 來(lái)加密用戶密碼,并提供 Go 語(yǔ)言實(shí)現(xiàn)示例,需要的朋友可以參考下
    2025-12-12
  • golang與非golang程序探測(cè)beyla源碼解讀

    golang與非golang程序探測(cè)beyla源碼解讀

    這篇文章主要為大家介紹了beyla源碼解讀之golang與非golang程序的探測(cè)實(shí)例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-12-12
  • Go語(yǔ)言nil標(biāo)識(shí)符(空值/零值)

    Go語(yǔ)言nil標(biāo)識(shí)符(空值/零值)

    本文主要介紹了Go語(yǔ)言nil標(biāo)識(shí)符(空值/零值),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2023-03-03
  • gin?session中間件使用及源碼流程分析

    gin?session中間件使用及源碼流程分析

    這篇文章主要為大家介紹了gin?session中間件使用及源碼分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-10-10
  • Go語(yǔ)言拼接URL路徑的三種方法

    Go語(yǔ)言拼接URL路徑的三種方法

    本文主要介紹了Go語(yǔ)言拼接URL路徑的三種方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2023-03-03

最新評(píng)論

巴彦县| 谷城县| 佛教| 修武县| 安岳县| 勃利县| 扎兰屯市| 乌鲁木齐市| 锦屏县| 泰来县| 淮安市| 行唐县| 大英县| 平远县| 苍溪县| 兴海县| 句容市| 蚌埠市| 郸城县| 许昌市| 宁晋县| 库尔勒市| 喜德县| 宣城市| 台中市| 炎陵县| 平邑县| 稷山县| 开封市| 崇信县| 南召县| 莱西市| 渭源县| 赣州市| 金平| 金川县| 醴陵市| 邻水| 新兴县| 阜城县| 成安县|