主頁 > 後端開發 > Go 語言之在 gin 框架中使用 zap 日志庫

Go 語言之在 gin 框架中使用 zap 日志庫

2023-06-18 07:48:21 後端開發

Go 語言之在 gin 框架中使用 zap 日志庫

gin 框架默認使用的是自帶的日志

gin.Default()的原始碼 Logger(), Recovery()

func Default() *Engine {
	debugPrintWARNINGDefault()
	engine := New()
	engine.Use(Logger(), Recovery())
	return engine
}

// Logger instances a Logger middleware that will write the logs to gin.DefaultWriter.
// By default, gin.DefaultWriter = os.Stdout.
func Logger() HandlerFunc {
	return LoggerWithConfig(LoggerConfig{})
}

// Recovery returns a middleware that recovers from any panics and writes a 500 if there was one.
func Recovery() HandlerFunc {
	return RecoveryWithWriter(DefaultErrorWriter)
}

// RecoveryWithWriter returns a middleware for a given writer that recovers from any panics and writes a 500 if there was one.
func RecoveryWithWriter(out io.Writer, recovery ...RecoveryFunc) HandlerFunc {
	if len(recovery) > 0 {
		return CustomRecoveryWithWriter(out, recovery[0])
	}
	return CustomRecoveryWithWriter(out, defaultHandleRecovery)
}

// CustomRecoveryWithWriter returns a middleware for a given writer that recovers from any panics and calls the provided handle func to handle it.
func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc {
	var logger *log.Logger
	if out != nil {
		logger = log.New(out, "\n\n\x1b[31m", log.LstdFlags)
	}
	return func(c *Context) {
		defer func() {
			if err := recover(); err != nil {
				// Check for a broken connection, as it is not really a
				// condition that warrants a panic stack trace.
				var brokenPipe bool
				if ne, ok := err.(*net.OpError); ok {
					var se *os.SyscallError
					if errors.As(ne, &se) {
						seStr := strings.ToLower(se.Error())
						if strings.Contains(seStr, "broken pipe") ||
							strings.Contains(seStr, "connection reset by peer") {
							brokenPipe = true
						}
					}
				}
				if logger != nil {
					stack := stack(3)
					httpRequest, _ := httputil.DumpRequest(c.Request, false)
					headers := strings.Split(string(httpRequest), "\r\n")
					for idx, header := range headers {
						current := strings.Split(header, ":")
						if current[0] == "Authorization" {
							headers[idx] = current[0] + ": *"
						}
					}
					headersToStr := strings.Join(headers, "\r\n")
					if brokenPipe {
						logger.Printf("%s\n%s%s", err, headersToStr, reset)
					} else if IsDebugging() {
						logger.Printf("[Recovery] %s panic recovered:\n%s\n%s\n%s%s",
							timeFormat(time.Now()), headersToStr, err, stack, reset)
					} else {
						logger.Printf("[Recovery] %s panic recovered:\n%s\n%s%s",
							timeFormat(time.Now()), err, stack, reset)
					}
				}
				if brokenPipe {
					// If the connection is dead, we can't write a status to it.
					c.Error(err.(error)) //nolint: errcheck
					c.Abort()
				} else {
					handle(c, err)
				}
			}
		}()
		c.Next()
	}
}

自定義 Logger(), Recovery()

實操

package main

import (
	"github.com/gin-gonic/gin"
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
	"gopkg.in/natefinch/lumberjack.v2"
	"net"
	"net/http"
	"net/http/httputil"
	"os"
	"runtime/debug"
	"strings"
	"time"
)

// 定義一個全域 logger 實體
// Logger提供快速、分級、結構化的日志記錄,所有方法對于并發使用都是安全的,
// Logger是為每一微秒和每一個分配都很重要的背景關系設計的,
// 因此它的API有意傾向于性能和型別安全,而不是簡便性,
// 對于大多數應用程式,SugaredLogger在性能和人體工程學之間取得了更好的平衡,
var logger *zap.Logger

// SugaredLogger將基本的Logger功能封裝在一個較慢但不那么冗長的API中,任何Logger都可以通過其Sugar方法轉換為sugardlogger,
//與Logger不同,SugaredLogger并不堅持結構化日志記錄,對于每個日志級別,它公開了四個方法:
//   - methods named after the log level for log.Print-style logging
//   - methods ending in "w" for loosely-typed structured logging
//   - methods ending in "f" for log.Printf-style logging
//   - methods ending in "ln" for log.Println-style logging

// For example, the methods for InfoLevel are:
//
//	Info(...any)           Print-style logging
//	Infow(...any)          Structured logging (read as "info with")
//	Infof(string, ...any)  Printf-style logging
//	Infoln(...any)         Println-style logging
var sugarLogger *zap.SugaredLogger

//func main() {
//	// 初始化
//	InitLogger()
//	// Sync呼叫底層Core的Sync方法,重繪所有緩沖的日志條目,應用程式在退出之前應該注意呼叫Sync,
//	// 在程式退出之前,把緩沖區里的日志刷到磁盤上
//	defer logger.Sync()
//	simpleHttpGet("www.baidu.com")
//	simpleHttpGet("http://www.baidu.com")
//
//	for i := 0; i < 10000; i++ {
//		logger.Info("test lumberjack for log rotate....")
//	}
//}

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

	r := gin.New()
	r.Use(GinLogger(logger), GinRecovery(logger, true))
	r.GET("/hello", func(c *gin.Context) {
		c.String(http.StatusOK, "hello xiaoqiao!")
	})
	r.Run()
}

// GinLogger
func GinLogger(logger *zap.Logger) gin.HandlerFunc {
	return func(c *gin.Context) {
		start := time.Now()
		path := c.Request.URL.Path
		query := c.Request.URL.RawQuery
		c.Next() // 執行后續中間件

		// Since returns the time elapsed since t.
		// It is shorthand for time.Now().Sub(t).
		cost := time.Since(start)
		logger.Info(path,
			zap.Int("status", c.Writer.Status()),
			zap.String("method", c.Request.Method),
			zap.String("path", path),
			zap.String("query", query),
			zap.String("ip", c.ClientIP()),
			zap.String("user-agent", c.Request.UserAgent()),
			zap.String("errors", c.Errors.ByType(gin.ErrorTypePrivate).String()),
			zap.Duration("cost", cost), // 運行時間
		)
	}
}

// GinRecovery
func GinRecovery(logger *zap.Logger, stack bool) gin.HandlerFunc {
	return func(c *gin.Context) {
		defer func() {
			if err := recover(); err != nil {
				// Check for a broken connection, as it is not really a
				// condition that warrants a panic stack trace.
				var brokenPipe bool
				if ne, ok := err.(*net.OpError); ok {
					if se, ok := ne.Err.(*os.SyscallError); ok {
						if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
							brokenPipe = true
						}
					}
				}

				httpRequest, _ := httputil.DumpRequest(c.Request, false)
				if brokenPipe {
					logger.Error(c.Request.URL.Path,
						zap.Any("error", err),
						zap.String("request", string(httpRequest)),
					)
					// If the connection is dead, we can't write a status to it.
					c.Error(err.(error)) // nolint: errcheck
					c.Abort()
					return
				}

				if stack {
					logger.Error("[Recovery from panic]",
						zap.Any("error", err),
						zap.String("request", string(httpRequest)),
						zap.String("stack", string(debug.Stack())),
					)
				} else {
					logger.Error("[Recovery from panic]",
						zap.Any("error", err),
						zap.String("request", string(httpRequest)),
					)
				}
				c.AbortWithStatus(http.StatusInternalServerError)
			}
		}()
		c.Next()
	}
}

func InitLogger() {
	writeSyncer := getLogWriter()
	encoder := getEncoder()
	// NewCore創建一個向WriteSyncer寫入日志的Core,

	// A WriteSyncer is an io.Writer that can also flush any buffered data. Note
	// that *os.File (and thus, os.Stderr and os.Stdout) implement WriteSyncer.

	// LevelEnabler決定在記錄訊息時是否啟用給定的日志級別,
	// Each concrete Level value implements a static LevelEnabler which returns
	// true for itself and all higher logging levels. For example WarnLevel.Enabled()
	// will return true for WarnLevel, ErrorLevel, DPanicLevel, PanicLevel, and
	// FatalLevel, but return false for InfoLevel and DebugLevel.
	core := zapcore.NewCore(encoder, writeSyncer, zapcore.DebugLevel)

	// New constructs a new Logger from the provided zapcore.Core and Options. If
	// the passed zapcore.Core is nil, it falls back to using a no-op
	// implementation.

	// AddCaller configures the Logger to annotate each message with the filename,
	// line number, and function name of zap's caller. See also WithCaller.
	logger = zap.New(core, zap.AddCaller())
	// Sugar封裝了Logger,以提供更符合人體工程學的API,但速度略慢,糖化一個Logger的成本非常低,
	// 因此一個應用程式同時使用Loggers和SugaredLoggers是合理的,在性能敏感代碼的邊界上在它們之間進行轉換,
	sugarLogger = logger.Sugar()
}

func getEncoder() zapcore.Encoder {
	// NewJSONEncoder創建了一個快速、低分配的JSON編碼器,編碼器適當地轉義所有欄位鍵和值,
	// NewProductionEncoderConfig returns an opinionated EncoderConfig for
	// production environments.
	//return zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig())

	// NewConsoleEncoder創建一個編碼器,其輸出是為人類而不是機器設計的,
	// 它以純文本格式序列化核心日志條目資料(訊息、級別、時間戳等),并將結構化背景關系保留為JSON,
	encoderConfig := zapcore.EncoderConfig{
		TimeKey:        "ts",
		LevelKey:       "level",
		NameKey:        "logger",
		CallerKey:      "caller",
		FunctionKey:    zapcore.OmitKey,
		MessageKey:     "msg",
		StacktraceKey:  "stacktrace",
		LineEnding:     zapcore.DefaultLineEnding,
		EncodeLevel:    zapcore.LowercaseLevelEncoder,
		EncodeTime:     zapcore.ISO8601TimeEncoder,
		EncodeDuration: zapcore.SecondsDurationEncoder,
		EncodeCaller:   zapcore.ShortCallerEncoder,
	}

	return zapcore.NewConsoleEncoder(encoderConfig)
}

//func getLogWriter() zapcore.WriteSyncer {
//	// Create創建或截斷指定檔案,如果檔案已經存在,它將被截斷,如果該檔案不存在,則以模式0666(在umask之前)創建,
//	// 如果成功,回傳的File上的方法可以用于IO;關聯的檔案描述符模式為O_RDWR,如果有一個錯誤,它的型別將是PathError,
//	//file, _ := os.Create("./test.log")
//	file, err := os.OpenFile("./test.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
//	if err != nil {
//		log.Fatalf("open log file failed with error: %v", err)
//	}
//	// AddSync converts an io.Writer to a WriteSyncer. It attempts to be
//	// intelligent: if the concrete type of the io.Writer implements WriteSyncer,
//	// we'll use the existing Sync method. If it doesn't, we'll add a no-op Sync.
//	return zapcore.AddSync(file)
//}

func getLogWriter() zapcore.WriteSyncer {
	// Logger is an io.WriteCloser that writes to the specified filename.
	// 日志記錄器在第一次寫入時打開或創建日志檔案,如果檔案存在并且小于MaxSize兆位元組,則lumberjack將打開并追加該檔案,
	// 如果該檔案存在并且其大小為>= MaxSize兆位元組,
	// 則通過將當前時間放在檔案擴展名(或者如果沒有擴展名則放在檔案名的末尾)的名稱中的時間戳中來重命名該檔案,
	// 然后使用原始檔案名創建一個新的日志檔案,
	// 每當寫操作導致當前日志檔案超過MaxSize兆位元組時,將關閉當前檔案,重新命名,并使用原始名稱創建新的日志檔案,
	// 因此,您給Logger的檔案名始終是“當前”日志檔案,
	// 如果MaxBackups和MaxAge均為0,則不會洗掉舊的日志檔案,
	lumberJackLogger := &lumberjack.Logger{
		// Filename是要寫入日志的檔案,備份日志檔案將保留在同一目錄下
		Filename: "./test.log",
		// MaxSize是日志檔案旋轉之前的最大大小(以兆位元組為單位),默認為100兆位元組,
		MaxSize: 1, // M
		// MaxBackups是要保留的舊日志檔案的最大數量,默認是保留所有舊的日志檔案(盡管MaxAge仍然可能導致它們被洗掉),
		MaxBackups: 5, // 備份數量
		// MaxAge是根據檔案名中編碼的時間戳保留舊日志檔案的最大天數,
		// 請注意,一天被定義為24小時,由于夏令時、閏秒等原因,可能與日歷日不完全對應,默認情況下,不根據時間洗掉舊的日志檔案,
		MaxAge: 30, // 備份天數
		// Compress決定是否應該使用gzip壓縮旋轉的日志檔案,默認情況下不執行壓縮,
		Compress: false, // 是否壓縮
	}

	return zapcore.AddSync(lumberJackLogger)
}

func simpleHttpGet(url string) {
	// Get向指定的URL發出Get命令,如果回應是以下重定向代碼之一,則Get跟隨重定向,最多可重定向10個:
	//	301 (Moved Permanently)
	//	302 (Found)
	//	303 (See Other)
	//	307 (Temporary Redirect)
	//	308 (Permanent Redirect)
	// Get is a wrapper around DefaultClient.Get.
	// 使用NewRequest和DefaultClient.Do來發出帶有自定義頭的請求,
	resp, err := http.Get(url)
	if err != nil {
		// Error在ErrorLevel記錄訊息,該訊息包括在日志站點傳遞的任何欄位,以及日志記錄器上積累的任何欄位,
		//logger.Error(

		// 錯誤使用fmt,以Sprint方式構造和記錄訊息,
		sugarLogger.Error(
			"Error fetching url..",
			zap.String("url", url), // 字串用給定的鍵和值構造一個欄位,
			zap.Error(err))         // // Error is shorthand for the common idiom NamedError("error", err).
	} else {
		// Info以infollevel記錄訊息,該訊息包括在日志站點傳遞的任何欄位,以及日志記錄器上積累的任何欄位,
		//logger.Info("Success..",

		// Info使用fmt,以Sprint方式構造和記錄訊息,
		sugarLogger.Info("Success..",
			zap.String("statusCode", resp.Status),
			zap.String("url", url))
		resp.Body.Close()
	}
}

運行并訪問:http://localhost:8080/hello

Code/go/zap_demo via ?? v1.20.3 via ?? base 
? go run main.go 
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /hello                    --> main.main.func1 (3 handlers)
[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
[GIN-debug] Environment variable PORT is undefined. Using port :8080 by default
[GIN-debug] Listening and serving HTTP on :8080

test.log

2023-06-17T14:17:08.553+0800	info	zap_demo/main.go:42	test lumberjack for log rotate....
2023-06-17T16:48:25.600+0800	info	zap_demo/main.go:76	/hello	{"status": 200, "method": "GET", "path": "/hello", "query": "", "ip": "::1", "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36", "errors": "", "cost": 0.000057417}
2023-06-17T16:48:25.753+0800	info	zap_demo/main.go:76	/favicon.ico	{"status": 404, "method": "GET", "path": "/favicon.ico", "query": "", "ip": "::1", "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36", "errors": "", "cost": 0.000000541}

其它參考:https://github.com/gin-contrib/zap

本文來自博客園,作者:尋月隱君,轉載請注明原文鏈接:https://www.cnblogs.com/QiaoPengjun/p/17488013.html

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/555446.html

標籤:其他

上一篇:Nginx 學習筆記

下一篇:返回列表

標籤雲
其他(161187) Python(38240) JavaScript(25504) Java(18245) C(15237) 區塊鏈(8271) C#(7972) AI(7469) 爪哇(7425) MySQL(7256) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5875) 数组(5741) R(5409) Linux(5347) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4601) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2436) ASP.NET(2404) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) .NET技术(1984) HtmlCss(1968) 功能(1967) Web開發(1951) C++(1941) python-3.x(1918) 弹簧靴(1913) xml(1889) PostgreSQL(1881) .NETCore(1863) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Go 語言之在 gin 框架中使用 zap 日志庫

    # Go 語言之在 gin 框架中使用 zap 日志庫 ### gin 框架默認使用的是自帶的日志 #### `gin.Default()`的原始碼 Logger(), Recovery() ```go func Default() *Engine { debugPrintWARNINGDefault ......

    uj5u.com 2023-06-18 07:48:21 more
  • Nginx 學習筆記

    ## 概述 Nginx 是一個高性能的 HTTP 和反向代理服務器,特點是占用記憶體少,并發能力強 #### 1. 正向代理 如果把局域網外的 Internet 想象成一個巨大的資源庫,則局域網中的客戶端要訪問 Internet,需要通過代理服務器來訪問,這種訪問就稱為正向代理 ![](https:/ ......

    uj5u.com 2023-06-18 07:48:13 more
  • Python潮流周刊#7:我討厭用 asyncio

    你好,我是貓哥。這里記錄每周值得分享的 Python 及通用技術內容,部分為英文,已在小標題注明。(標題取自其中一則分享,不代表全部內容都是該主題,特此宣告。) 首發于我的博客:[https://pythoncat.top/posts/2023-06-17-weekly7](https://pyth ......

    uj5u.com 2023-06-18 07:47:39 more
  • Scala面向物件

    # 類和物件 **組成結構** ? 建構式: 在創建物件的時候給屬性賦值 ? 成員變數: ? 成員方法(函式) ? 區域變數 ? 代碼塊 ## 構造器 每個類都有一個主構造器,這個構造器和類定義"交織"在一起類名后面的內容就是主構造器,如果引數串列為空的話,()可以省略 scala的類有且僅有一個 ......

    uj5u.com 2023-06-18 07:42:11 more
  • Django學習筆記

    ## 1.常用命令 `創建專案:django-admin startproject 專案名` `創建APP(進入工程目錄):python manage.py startapp 網站名` `創建庫表(進入工程目錄):python manage.py makemigrations` `執行庫表建立(進入 ......

    uj5u.com 2023-06-18 07:40:09 more
  • Go 語言之自定義 zap 日志

    # Go 語言之自定義 zap 日志 [zap 日志](https://github.com/uber-go/zap):https://github.com/uber-go/zap ## 一、日志寫入檔案 - `zap.NewProduction`、`zap.NewDevelopment` 是預設配 ......

    uj5u.com 2023-06-18 07:40:00 more
  • Spring Boot實作高質量的CRUD-5

    (續前文) ## 9、Service實作類代碼示例 ? ? 以用戶管理模塊為例,展示Service實作類代碼。用戶管理的Service實作類為UserManServiceImpl。?UserManServiceImpl除了沒有deleteItems方法外,具備CRUD的其它常規方法。實際上?User ......

    uj5u.com 2023-06-18 07:39:53 more
  • ?任務編排工具在之家業務系統中應用探究

    本文主要介紹在之家廣告業務系統中運用任務編排治理工具的場景及其可以解決的問題,講解任務編排框架的核心要點,以及向大家演示一個任務編排框架的基本結構,讓大家對任務編排工具增強業務開發效率,提高研發質量有更深刻的理解。 ......

    uj5u.com 2023-06-18 07:39:37 more
  • Go 語言之 zap 日志庫簡單使用

    # Go 語言之 zap 日志庫簡單使用 ## 默認的 Go log log:https://pkg.go.dev/log ```go package main import ( "log" "os" ) func init() { log.SetPrefix("LOG: ") // 設定前綴 f, ......

    uj5u.com 2023-06-18 07:39:24 more
  • Scala面向物件

    # 類和物件 **組成結構** ? 建構式: 在創建物件的時候給屬性賦值 ? 成員變數: ? 成員方法(函式) ? 區域變數 ? 代碼塊 ## 構造器 每個類都有一個主構造器,這個構造器和類定義"交織"在一起類名后面的內容就是主構造器,如果引數串列為空的話,()可以省略 scala的類有且僅有一個 ......

    uj5u.com 2023-06-18 07:39:00 more