mirror of
https://gitee.com/johng/gf
synced 2026-07-08 14:39:50 +08:00
add package gbuild
This commit is contained in:
9
.example/os/gbuild/config.toml
Normal file
9
.example/os/gbuild/config.toml
Normal file
@ -0,0 +1,9 @@
|
||||
|
||||
|
||||
# custom gf build setting.
|
||||
[compiler]
|
||||
name = "app"
|
||||
[compiler.varmap]
|
||||
name = "GoFrame"
|
||||
version = "1.10.1"
|
||||
home-site = "https://goframe.org"
|
||||
11
.example/os/gbuild/gbuild.go
Normal file
11
.example/os/gbuild/gbuild.go
Normal file
@ -0,0 +1,11 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/gogf/gf/frame/g"
|
||||
"github.com/gogf/gf/os/gbuild"
|
||||
)
|
||||
|
||||
func main() {
|
||||
g.Dump(gbuild.Info())
|
||||
g.Dump(gbuild.Map())
|
||||
}
|
||||
@ -2,28 +2,12 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gogf/gf/debug/gdebug"
|
||||
"github.com/gogf/gf/frame/g"
|
||||
"github.com/gogf/gf/util/gconv"
|
||||
"reflect"
|
||||
"os"
|
||||
)
|
||||
|
||||
// 结构体
|
||||
type Data struct {
|
||||
Name string `json:"name"`
|
||||
Age int `json:"age"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
data := Data{Name: "abcdefg"}
|
||||
data1 := Data{}
|
||||
data2 := Data{}
|
||||
|
||||
g.Redis().Do("SET", "goods:id", data)
|
||||
v, _ := g.Redis().DoVar("GET", "goods:id")
|
||||
|
||||
v.Struct(&data1)
|
||||
gconv.Struct(v, &data2)
|
||||
|
||||
fmt.Println(v, data1, data2)
|
||||
fmt.Println(reflect.TypeOf(v), reflect.TypeOf(data1))
|
||||
g.Dump(os.Args)
|
||||
fmt.Println(gdebug.BuildInfo())
|
||||
}
|
||||
|
||||
@ -1,17 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gogf/gf/frame/g"
|
||||
"github.com/gogf/gf/util/gconv"
|
||||
)
|
||||
|
||||
func main() {
|
||||
type User struct {
|
||||
Scores []int64
|
||||
}
|
||||
user := new(User)
|
||||
err := gconv.Struct(g.Map{"scores": []interface{}{1, 2, 3}}, user)
|
||||
fmt.Println(err)
|
||||
fmt.Println(user)
|
||||
}
|
||||
@ -233,15 +233,7 @@ func (v *Var) Map(tags ...string) map[string]interface{} {
|
||||
|
||||
// MapStrStr converts <v> to map[string]string.
|
||||
func (v *Var) MapStrStr(tags ...string) map[string]string {
|
||||
m := v.Map(tags...)
|
||||
if len(m) > 0 {
|
||||
vMap := make(map[string]string)
|
||||
for k, v := range m {
|
||||
vMap[k] = gconv.String(v)
|
||||
}
|
||||
return vMap
|
||||
}
|
||||
return nil
|
||||
return gconv.MapStrStr(v.Val(), tags...)
|
||||
}
|
||||
|
||||
// MapStrVar converts <v> to map[string]*Var.
|
||||
@ -264,15 +256,7 @@ func (v *Var) MapDeep(tags ...string) map[string]interface{} {
|
||||
|
||||
// MapDeep converts <v> to map[string]string recursively.
|
||||
func (v *Var) MapStrStrDeep(tags ...string) map[string]string {
|
||||
m := v.MapDeep(tags...)
|
||||
if len(m) > 0 {
|
||||
vMap := make(map[string]string)
|
||||
for k, v := range m {
|
||||
vMap[k] = gconv.String(v)
|
||||
}
|
||||
return vMap
|
||||
}
|
||||
return nil
|
||||
return gconv.MapStrStrDeep(v.Val(), tags...)
|
||||
}
|
||||
|
||||
// MapStrVarDeep converts <v> to map[string]*Var recursively.
|
||||
|
||||
@ -10,7 +10,6 @@ package gdebug
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/gogf/gf"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
@ -29,9 +28,6 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
buildTime = "" // Binary time string, which is injected from "go build -ldflags '-X importpath.name=value'".
|
||||
buildGoVersion = "" // Binary go version, which is injected from "go build -ldflags '-X importpath.name=value'".
|
||||
buildGitCommit = "" // Binary git commit, which is injected from "go build -ldflags '-X importpath.name=value'".
|
||||
goRootForFilter = runtime.GOROOT() // goRootForFilter is used for stack filtering purpose.
|
||||
binaryVersion = "" // The version of current running binary(uint64 hex).
|
||||
binaryVersionMd5 = "" // The version of current running binary(MD5).
|
||||
@ -43,18 +39,6 @@ func init() {
|
||||
}
|
||||
}
|
||||
|
||||
// BuildInfo returns the built information of the binary.
|
||||
// Note that it should be used with gf-cli tool: gf build,
|
||||
// which injects necessary information into the binary.
|
||||
func BuildInfo() map[string]string {
|
||||
return map[string]string{
|
||||
"gf": gf.VERSION,
|
||||
"go": buildGoVersion,
|
||||
"git": buildGitCommit,
|
||||
"time": buildTime,
|
||||
}
|
||||
}
|
||||
|
||||
// BinVersion returns the version of current running binary.
|
||||
// It uses ghash.BKDRHash+BASE36 algorithm to calculate the unique version of the binary.
|
||||
func BinVersion() string {
|
||||
|
||||
@ -78,7 +78,7 @@ func (j *Json) GetVars(pattern string, def ...interface{}) []*gvar.Var {
|
||||
return gvar.New(j.Get(pattern, def...)).Vars()
|
||||
}
|
||||
|
||||
// GetMap gets the value by specified <pattern>,
|
||||
// GetMap retrieves the value by specified <pattern>,
|
||||
// and converts it to map[string]interface{}.
|
||||
func (j *Json) GetMap(pattern string, def ...interface{}) map[string]interface{} {
|
||||
result := j.Get(pattern, def...)
|
||||
@ -88,6 +88,16 @@ func (j *Json) GetMap(pattern string, def ...interface{}) map[string]interface{}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetMapStrStr retrieves the value by specified <pattern>,
|
||||
// and converts it to map[string]string.
|
||||
func (j *Json) GetMapStrStr(pattern string, def ...interface{}) map[string]string {
|
||||
result := j.Get(pattern, def...)
|
||||
if result != nil {
|
||||
return gconv.MapStrStr(result)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetJson gets the value by specified <pattern>,
|
||||
// and converts it to a un-concurrent-safe Json object.
|
||||
func (j *Json) GetJson(pattern string, def ...interface{}) *Json {
|
||||
|
||||
72
os/gbuild/gbuild.go
Normal file
72
os/gbuild/gbuild.go
Normal file
@ -0,0 +1,72 @@
|
||||
// Copyright 2019 gf Author(https://github.com/gogf/gf). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
// Package gbuild manages the build-in variables from "gf build".
|
||||
package gbuild
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/gogf/gf"
|
||||
"github.com/gogf/gf/container/gvar"
|
||||
"github.com/gogf/gf/encoding/gbase64"
|
||||
"github.com/gogf/gf/internal/intlog"
|
||||
"github.com/gogf/gf/util/gconv"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
var (
|
||||
builtInVarStr = "" // Raw variable base64 string.
|
||||
builtInVarMap = map[string]interface{}{} // Binary custom variable map decoded.
|
||||
)
|
||||
|
||||
func init() {
|
||||
if builtInVarStr != "" {
|
||||
err := json.Unmarshal(gbase64.MustDecodeString(builtInVarStr), &builtInVarMap)
|
||||
if err != nil {
|
||||
intlog.Error(err)
|
||||
}
|
||||
builtInVarMap["gfVersion"] = gf.VERSION
|
||||
builtInVarMap["goVersion"] = runtime.Version()
|
||||
}
|
||||
}
|
||||
|
||||
// Info returns the basic built information of the binary as map.
|
||||
// Note that it should be used with gf-cli tool "gf build",
|
||||
// which injects necessary information into the binary.
|
||||
func Info() map[string]string {
|
||||
return map[string]string{
|
||||
"gf": GetString("gfVersion"),
|
||||
"go": GetString("goVersion"),
|
||||
"git": GetString("builtGit"),
|
||||
"time": GetString("builtTime"),
|
||||
}
|
||||
}
|
||||
|
||||
// Get retrieves and returns the build-in binary variable with given name.
|
||||
func Get(name string, def ...interface{}) interface{} {
|
||||
if v, ok := builtInVarMap[name]; ok {
|
||||
return v
|
||||
}
|
||||
if len(def) > 0 {
|
||||
return def[0]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get retrieves and returns the build-in binary variable of given name as *gvar.Var.
|
||||
func GetVar(name string, def ...interface{}) *gvar.Var {
|
||||
return gvar.New(Get(name, def...))
|
||||
}
|
||||
|
||||
// GetString retrieves and returns the build-in binary variable of given name as string.
|
||||
func GetString(name string, def ...interface{}) string {
|
||||
return gconv.String(Get(name, def...))
|
||||
}
|
||||
|
||||
// Map returns the custom build-in variable map.
|
||||
func Map() map[string]interface{} {
|
||||
return builtInVarMap
|
||||
}
|
||||
@ -44,6 +44,13 @@ func (c *Config) GetMap(pattern string, def ...interface{}) map[string]interface
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Config) GetMapStrStr(pattern string, def ...interface{}) map[string]string {
|
||||
if j := c.getJson(); j != nil {
|
||||
return j.GetMapStrStr(pattern, def...)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Config) GetArray(pattern string, def ...interface{}) []interface{} {
|
||||
if j := c.getJson(); j != nil {
|
||||
return j.GetArray(pattern, def...)
|
||||
|
||||
@ -13,7 +13,6 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/os/gfile"
|
||||
@ -36,7 +35,7 @@ func Pid() int {
|
||||
return processPid
|
||||
}
|
||||
|
||||
// 获取父进程ID(gproc父进程,如果当前进程本身就是父进程,那么返回自身的pid,不存在时则使用系统父进程)
|
||||
// PPid returns the custom parent pid if exists, or else it returns the system parent pid.
|
||||
func PPid() int {
|
||||
if !IsChild() {
|
||||
return Pid()
|
||||
@ -48,18 +47,22 @@ func PPid() int {
|
||||
return PPidOS()
|
||||
}
|
||||
|
||||
// 获取父进程ID(系统父进程)
|
||||
// PPidOS returns the system parent pid of current process.
|
||||
// Note that the difference between PPidOS and PPid function is that the PPidOS returns
|
||||
// the system ppid, but the PPid functions may return the custom pid by gproc if the custom
|
||||
// ppid exists.
|
||||
func PPidOS() int {
|
||||
return os.Getppid()
|
||||
}
|
||||
|
||||
// 判断当前进程是否为gproc创建的子进程
|
||||
// IsChild checks and returns whether current process is a child process.
|
||||
// A child process is forked by another gproc process.
|
||||
func IsChild() bool {
|
||||
ppidValue := os.Getenv(gPROC_ENV_KEY_PPID_KEY)
|
||||
return ppidValue != "" && ppidValue != "0"
|
||||
}
|
||||
|
||||
// 设置gproc父进程ID,当ppid为0时表示该进程为gproc主进程,否则为gproc子进程
|
||||
// SetPPid sets custom parent pid for current process.
|
||||
func SetPPid(ppid int) error {
|
||||
if ppid > 0 {
|
||||
return os.Setenv(gPROC_ENV_KEY_PPID_KEY, gconv.String(ppid))
|
||||
@ -68,50 +71,84 @@ func SetPPid(ppid int) error {
|
||||
}
|
||||
}
|
||||
|
||||
// 进程开始执行时间
|
||||
// StartTime returns the start time of current process.
|
||||
func StartTime() time.Time {
|
||||
return processStartTime
|
||||
}
|
||||
|
||||
// 进程已经运行的时间(毫秒)
|
||||
func Uptime() int {
|
||||
return int(time.Now().UnixNano()/1e6 - processStartTime.UnixNano()/1e6)
|
||||
// Uptime returns the duration which current process has been running
|
||||
func Uptime() time.Duration {
|
||||
return time.Now().Sub(processStartTime)
|
||||
}
|
||||
|
||||
// 阻塞执行shell指令,并给定输入输出对象
|
||||
// Shell executes command <cmd> synchronizingly with given input pipe <in> and output pipe <out>.
|
||||
// The command <cmd> reads the input parameters from input pipe <in>, and writes its output automatically
|
||||
// to output pipe <out>.
|
||||
func Shell(cmd string, out io.Writer, in io.Reader) error {
|
||||
p := NewProcess(getShell(), []string{getShellOption(), cmd})
|
||||
p := NewProcess(getShell(), append([]string{getShellOption()}, parseCommand(cmd)...))
|
||||
p.Stdin = in
|
||||
p.Stdout = out
|
||||
return p.Run()
|
||||
}
|
||||
|
||||
// 阻塞执行shell指令,并输出结果当终端(如果需要异步,请使用goroutine)
|
||||
// ShellRun executes given command <cmd> synchronizingly and outputs the command result to the stdout.
|
||||
func ShellRun(cmd string) error {
|
||||
p := NewProcess(getShell(), []string{getShellOption(), cmd})
|
||||
p := NewProcess(getShell(), append([]string{getShellOption()}, parseCommand(cmd)...))
|
||||
return p.Run()
|
||||
}
|
||||
|
||||
// 阻塞执行shell指令,并返回输出结果(如果需要异步,请使用goroutine)
|
||||
// ShellExec executes given command <cmd> synchronizingly and returns the command result.
|
||||
func ShellExec(cmd string, environment ...[]string) (string, error) {
|
||||
buf := bytes.NewBuffer(nil)
|
||||
p := NewProcess(getShell(), []string{getShellOption(), cmd}, environment...)
|
||||
p := NewProcess(getShell(), append([]string{getShellOption()}, parseCommand(cmd)...), environment...)
|
||||
p.Stdout = buf
|
||||
err := p.Run()
|
||||
return buf.String(), err
|
||||
}
|
||||
|
||||
// 检测环境变量中是否已经存在指定键名
|
||||
func checkEnvKey(env []string, key string) bool {
|
||||
for _, v := range env {
|
||||
if len(v) >= len(key) && strings.EqualFold(v[0:len(key)], key) {
|
||||
return true
|
||||
// parseCommand parses command <cmd> into slice arguments.
|
||||
//
|
||||
// Note that it just parses the <cmd> for "cmd.exe" binary in windows, but it is not necessary
|
||||
// parsing the <cmd> for other systems using "bash"/"sh" binary.
|
||||
func parseCommand(cmd string) (args []string) {
|
||||
if runtime.GOOS != "windows" {
|
||||
return []string{cmd}
|
||||
}
|
||||
// Just for "cmd.exe" in windows.
|
||||
var argStr string
|
||||
var firstChar, prevChar, lastChar1, lastChar2 byte
|
||||
array := gstr.SplitAndTrim(cmd, " ")
|
||||
for _, v := range array {
|
||||
if len(argStr) > 0 {
|
||||
argStr += " "
|
||||
}
|
||||
firstChar = v[0]
|
||||
lastChar1 = v[len(v)-1]
|
||||
lastChar2 = 0
|
||||
if len(v) > 1 {
|
||||
lastChar2 = v[len(v)-2]
|
||||
}
|
||||
if prevChar == 0 && (firstChar == '"' || firstChar == '\'') {
|
||||
// It should remove the first quote char.
|
||||
argStr += v[1:]
|
||||
prevChar = firstChar
|
||||
} else if prevChar != 0 && lastChar2 != '\\' && lastChar1 == prevChar {
|
||||
// It should remove the last quote char.
|
||||
argStr += v[:len(v)-1]
|
||||
args = append(args, argStr)
|
||||
argStr = ""
|
||||
prevChar = 0
|
||||
} else if len(argStr) > 0 {
|
||||
argStr += v
|
||||
} else {
|
||||
args = append(args, v)
|
||||
}
|
||||
}
|
||||
return false
|
||||
return
|
||||
}
|
||||
|
||||
// 获取当前系统下的shell路径
|
||||
// getShell returns the shell command depending on current working operation system.
|
||||
// It returns "cmd.exe" for windows, and "bash" or "sh" for others.
|
||||
func getShell() string {
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
@ -125,7 +162,8 @@ func getShell() string {
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前系统默认shell执行指令的option参数
|
||||
// getShellOption returns the shell option depending on current working operation system.
|
||||
// It returns "/c" for windows, and "-c" for others.
|
||||
func getShellOption() string {
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
@ -135,7 +173,7 @@ func getShellOption() string {
|
||||
}
|
||||
}
|
||||
|
||||
// 从环境变量PATH中搜索可执行文件
|
||||
// SearchBinary searches the binary <file> in current working folder and PATH environment.
|
||||
func SearchBinary(file string) string {
|
||||
// Check if it's absolute path of exists at current working directory.
|
||||
if gfile.Exists(file) {
|
||||
|
||||
@ -14,14 +14,14 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 子进程
|
||||
// Process is the struct for a single process.
|
||||
type Process struct {
|
||||
exec.Cmd
|
||||
Manager *Manager // 所属进程管理器
|
||||
PPid int // 自定义关联的父进程ID
|
||||
Manager *Manager
|
||||
PPid int
|
||||
}
|
||||
|
||||
// 创建一个进程(不执行)
|
||||
// NewProcess creates and returns a new Process.
|
||||
func NewProcess(path string, args []string, environment ...[]string) *Process {
|
||||
var env []string
|
||||
if len(environment) > 0 {
|
||||
@ -57,10 +57,11 @@ func NewProcess(path string, args []string, environment ...[]string) *Process {
|
||||
|
||||
// NewProcessCmd creates and returns a process with given command and optional environment variable array.
|
||||
func NewProcessCmd(cmd string, environment ...[]string) *Process {
|
||||
return NewProcess(getShell(), []string{getShellOption(), cmd}, environment...)
|
||||
return NewProcess(getShell(), append([]string{getShellOption()}, parseCommand(cmd)...), environment...)
|
||||
}
|
||||
|
||||
// 开始执行(非阻塞)
|
||||
// Start starts executing the process in non-blocking way.
|
||||
// It returns the pid if success, or else it returns an error.
|
||||
func (p *Process) Start() (int, error) {
|
||||
if p.Process != nil {
|
||||
return p.Pid(), nil
|
||||
@ -76,7 +77,7 @@ func (p *Process) Start() (int, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// 运行进程(阻塞等待执行完毕)
|
||||
// Run executes the process in blocking way.
|
||||
func (p *Process) Run() error {
|
||||
if _, err := p.Start(); err == nil {
|
||||
return p.Wait()
|
||||
@ -93,7 +94,7 @@ func (p *Process) Pid() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
// 向进程发送消息
|
||||
// Send send custom data to the process.
|
||||
func (p *Process) Send(data []byte) error {
|
||||
if p.Process != nil {
|
||||
return Send(p.Process.Pid, data)
|
||||
|
||||
26
os/gproc/gproc_unit_basic_test.go
Normal file
26
os/gproc/gproc_unit_basic_test.go
Normal file
@ -0,0 +1,26 @@
|
||||
// Copyright 2019 gf Author(https://github.com/gogf/gf). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package gproc
|
||||
|
||||
import (
|
||||
"github.com/gogf/gf/test/gtest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_parseCommand(t *testing.T) {
|
||||
gtest.Case(t, func() {
|
||||
commandMap := map[string]interface{}{
|
||||
`cmd`: []string{`cmd`},
|
||||
`cmd /c`: []string{`cmd`, `/c`},
|
||||
`cmd /c go build`: []string{`cmd`, `/c`, `go`, `build`},
|
||||
`cmd /c go build -ldflags "-X 'a=123' -X 'b=456'" test.go`: []string{`cmd`, `/c`, `go`, `build`, `-ldflags`, `"-X 'a=123' -X 'b=456'"`, `test.go`},
|
||||
}
|
||||
for k, v := range commandMap {
|
||||
gtest.Assert(parseCommand(k), v)
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -196,6 +196,14 @@ func (t *Time) Truncate(d time.Duration) *Time {
|
||||
return t
|
||||
}
|
||||
|
||||
// Sub returns the duration t-u. If the result exceeds the maximum (or minimum)
|
||||
// value that can be stored in a Duration, the maximum (or minimum) duration
|
||||
// will be returned.
|
||||
// To compute t-d for a duration d, use t.Add(-d).
|
||||
func (t *Time) Sub(u *Time) time.Duration {
|
||||
return t.Time.Sub(u.Time)
|
||||
}
|
||||
|
||||
// MarshalJSON implements the interface MarshalJSON for json.Marshal.
|
||||
func (t *Time) MarshalJSON() ([]byte, error) {
|
||||
return []byte(`"` + t.String() + `"`), nil
|
||||
|
||||
@ -190,6 +190,40 @@ func MapDeep(value interface{}, tags ...string) map[string]interface{} {
|
||||
return data
|
||||
}
|
||||
|
||||
// MapStrStr converts <value> to map[string]string.
|
||||
// Note that there might be data copy for this map type converting.
|
||||
func MapStrStr(value interface{}, tags ...string) map[string]string {
|
||||
if r, ok := value.(map[string]string); ok {
|
||||
return r
|
||||
}
|
||||
m := Map(value, tags...)
|
||||
if len(m) > 0 {
|
||||
vMap := make(map[string]string)
|
||||
for k, v := range m {
|
||||
vMap[k] = String(v)
|
||||
}
|
||||
return vMap
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MapStrStrDeep converts <value> to map[string]string recursively.
|
||||
// Note that there might be data copy for this map type converting.
|
||||
func MapStrStrDeep(value interface{}, tags ...string) map[string]string {
|
||||
if r, ok := value.(map[string]string); ok {
|
||||
return r
|
||||
}
|
||||
m := MapDeep(value, tags...)
|
||||
if len(m) > 0 {
|
||||
vMap := make(map[string]string)
|
||||
for k, v := range m {
|
||||
vMap[k] = String(v)
|
||||
}
|
||||
return vMap
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MapToMap converts map type variable <params> to another map type variable <pointer> using reflect.
|
||||
// The elements of <pointer> should be type of *map.
|
||||
func MapToMap(params interface{}, pointer interface{}, mapping ...map[string]string) error {
|
||||
|
||||
Reference in New Issue
Block a user