package types
import (
"go/ast"
"go/constant"
"go/token"
"internal/buildcfg"
. "internal/types/errors"
"slices"
)
func (check *Checker ) funcBody (decl *declInfo , name string , sig *Signature , body *ast .BlockStmt , iota constant .Value ) {
if check .conf .IgnoreFuncBodies {
panic ("function body not ignored" )
}
if check .conf ._Trace {
check .trace (body .Pos (), "-- %s: %s" , name , sig )
}
defer func (env environment , indent int ) {
check .environment = env
check .indent = indent
}(check .environment , check .indent )
check .environment = environment {
decl : decl ,
scope : sig .scope ,
version : check .version ,
iota : iota ,
sig : sig ,
}
check .indent = 0
check .stmtList (0 , body .List )
if check .hasLabel {
check .labels (body )
}
if sig .results .Len () > 0 && !check .isTerminating (body , "" ) {
check .error (atPos (body .Rbrace ), MissingReturn , "missing return" )
}
check .usage (sig .scope )
}
func (check *Checker ) usage (scope *Scope ) {
var unused []*Var
for name , elem := range scope .elems {
elem = resolve (name , elem )
if v , _ := elem .(*Var ); v != nil && !v .used {
unused = append (unused , v )
}
}
slices .SortFunc (unused , func (a , b *Var ) int {
return cmpPos (a .pos , b .pos )
})
for _ , v := range unused {
check .softErrorf (v , UnusedVar , "declared and not used: %s" , v .name )
}
for _ , scope := range scope .children {
if !scope .isFunc {
check .usage (scope )
}
}
}
type stmtContext uint
const (
breakOk stmtContext = 1 << iota
continueOk
fallthroughOk
finalSwitchCase
inTypeSwitch
)
func (check *Checker ) simpleStmt (s ast .Stmt ) {
if s != nil {
check .stmt (0 , s )
}
}
func trimTrailingEmptyStmts(list []ast .Stmt ) []ast .Stmt {
for i := len (list ); i > 0 ; i -- {
if _ , ok := list [i -1 ].(*ast .EmptyStmt ); !ok {
return list [:i ]
}
}
return nil
}
func (check *Checker ) stmtList (ctxt stmtContext , list []ast .Stmt ) {
ok := ctxt &fallthroughOk != 0
inner := ctxt &^ fallthroughOk
list = trimTrailingEmptyStmts (list )
for i , s := range list {
inner := inner
if ok && i +1 == len (list ) {
inner |= fallthroughOk
}
check .stmt (inner , s )
}
}
func (check *Checker ) multipleDefaults (list []ast .Stmt ) {
var first ast .Stmt
for _ , s := range list {
var d ast .Stmt
switch c := s .(type ) {
case *ast .CaseClause :
if len (c .List ) == 0 {
d = s
}
case *ast .CommClause :
if c .Comm == nil {
d = s
}
default :
check .error (s , InvalidSyntaxTree , "case/communication clause expected" )
}
if d != nil {
if first != nil {
check .errorf (d , DuplicateDefault , "multiple defaults (first at %s)" , check .fset .Position (first .Pos ()))
} else {
first = d
}
}
}
}
func (check *Checker ) openScope (node ast .Node , comment string ) {
scope := NewScope (check .scope , node .Pos (), node .End (), comment )
check .recordScope (node , scope )
check .scope = scope
}
func (check *Checker ) closeScope () {
check .scope = check .scope .Parent ()
}
func assignOp(op token .Token ) token .Token {
if token .ADD_ASSIGN <= op && op <= token .AND_NOT_ASSIGN {
return op + (token .ADD - token .ADD_ASSIGN )
}
return token .ILLEGAL
}
func (check *Checker ) suspendedCall (keyword string , call *ast .CallExpr ) {
var x operand
var msg string
var code Code
switch check .rawExpr (nil , &x , call , nil , false ) {
case conversion :
msg = "requires function call, not conversion"
code = InvalidDefer
if keyword == "go" {
code = InvalidGo
}
case expression :
msg = "discards result of"
code = UnusedResults
case statement :
return
default :
panic ("unreachable" )
}
check .errorf (&x , code , "%s %s %s" , keyword , msg , &x )
}
func goVal(val constant .Value ) any {
if val == nil {
return nil
}
switch val .Kind () {
case constant .Int :
if x , ok := constant .Int64Val (val ); ok {
return x
}
if x , ok := constant .Uint64Val (val ); ok {
return x
}
case constant .Float :
if x , ok := constant .Float64Val (val ); ok {
return x
}
case constant .String :
return constant .StringVal (val )
}
return nil
}
type (
valueMap map [any ][]valueType
valueType struct {
pos token .Pos
typ Type
}
)
func (check *Checker ) caseValues (x *operand , values []ast .Expr , seen valueMap ) {
L :
for _ , e := range values {
var v operand
check .expr (nil , &v , e )
if x .mode == invalid || v .mode == invalid {
continue L
}
check .convertUntyped (&v , x .typ )
if v .mode == invalid {
continue L
}
res := v
check .comparison (&res , x , token .EQL , true )
if res .mode == invalid {
continue L
}
if v .mode != constant_ {
continue L
}
if val := goVal (v .val ); val != nil {
for _ , vt := range seen [val ] {
if Identical (v .typ , vt .typ ) {
err := check .newError (DuplicateCase )
err .addf (&v , "duplicate case %s in expression switch" , &v )
err .addf (atPos (vt .pos ), "previous case" )
err .report ()
continue L
}
}
seen [val ] = append (seen [val ], valueType {v .Pos (), v .typ })
}
}
}
func (check *Checker ) isNil (e ast .Expr ) bool {
if name , _ := ast .Unparen (e ).(*ast .Ident ); name != nil {
_ , ok := check .lookup (name .Name ).(*Nil )
return ok
}
return false
}
func (check *Checker ) caseTypes (x *operand , types []ast .Expr , seen map [Type ]ast .Expr ) Type {
var T Type
var dummy operand
L :
for _ , e := range types {
if check .isNil (e ) {
T = nil
check .expr (nil , &dummy , e )
} else {
T = check .varType (e )
if !isValid (T ) {
continue L
}
}
for t , other := range seen {
if T == nil && t == nil || T != nil && t != nil && Identical (T , t ) {
Ts := "nil"
if T != nil {
Ts = TypeString (T , check .qualifier )
}
err := check .newError (DuplicateCase )
err .addf (e , "duplicate case %s in type switch" , Ts )
err .addf (other , "previous case" )
err .report ()
continue L
}
}
seen [T ] = e
if x != nil && T != nil {
check .typeAssertion (e , x , T , true )
}
}
if len (types ) != 1 || T == nil {
T = Typ [Invalid ]
if x != nil {
T = x .typ
}
}
assert (T != nil )
return T
}
func (check *Checker ) caseTypes_currently_unused (x *operand , xtyp *Interface , types []ast .Expr , seen map [string ]ast .Expr ) Type {
var T Type
var dummy operand
L :
for _ , e := range types {
var hash string
if check .isNil (e ) {
check .expr (nil , &dummy , e )
T = nil
hash = "<nil>"
} else {
T = check .varType (e )
if !isValid (T ) {
continue L
}
panic ("enable typeHash(T, nil)" )
}
if other := seen [hash ]; other != nil {
Ts := "nil"
if T != nil {
Ts = TypeString (T , check .qualifier )
}
err := check .newError (DuplicateCase )
err .addf (e , "duplicate case %s in type switch" , Ts )
err .addf (other , "previous case" )
err .report ()
continue L
}
seen [hash ] = e
if T != nil {
check .typeAssertion (e , x , T , true )
}
}
if len (types ) != 1 || T == nil {
T = Typ [Invalid ]
if x != nil {
T = x .typ
}
}
assert (T != nil )
return T
}
func (check *Checker ) stmt (ctxt stmtContext , s ast .Stmt ) {
if debug {
defer func (scope *Scope ) {
if p := recover (); p != nil {
panic (p )
}
assert (scope == check .scope )
}(check .scope )
}
defer check .processDelayed (len (check .delayed ))
inner := ctxt &^ (fallthroughOk | finalSwitchCase | inTypeSwitch )
switch s := s .(type ) {
case *ast .BadStmt , *ast .EmptyStmt :
case *ast .DeclStmt :
check .declStmt (s .Decl )
case *ast .LabeledStmt :
check .hasLabel = true
check .stmt (ctxt , s .Stmt )
case *ast .ExprStmt :
var x operand
kind := check .rawExpr (nil , &x , s .X , nil , false )
var msg string
var code Code
switch x .mode {
default :
if kind == statement {
return
}
msg = "is not used"
code = UnusedExpr
case builtin :
msg = "must be called"
code = UncalledBuiltin
case typexpr :
msg = "is not an expression"
code = NotAnExpr
}
check .errorf (&x , code , "%s %s" , &x , msg )
case *ast .SendStmt :
var ch , val operand
check .expr (nil , &ch , s .Chan )
check .expr (nil , &val , s .Value )
if ch .mode == invalid || val .mode == invalid {
return
}
u := coreType (ch .typ )
if u == nil {
check .errorf (inNode (s , s .Arrow ), InvalidSend , invalidOp +"cannot send to %s: no core type" , &ch )
return
}
uch , _ := u .(*Chan )
if uch == nil {
check .errorf (inNode (s , s .Arrow ), InvalidSend , invalidOp +"cannot send to non-channel %s" , &ch )
return
}
if uch .dir == RecvOnly {
check .errorf (inNode (s , s .Arrow ), InvalidSend , invalidOp +"cannot send to receive-only channel %s" , &ch )
return
}
check .assignment (&val , uch .elem , "send" )
case *ast .IncDecStmt :
var op token .Token
switch s .Tok {
case token .INC :
op = token .ADD
case token .DEC :
op = token .SUB
default :
check .errorf (inNode (s , s .TokPos ), InvalidSyntaxTree , "unknown inc/dec operation %s" , s .Tok )
return
}
var x operand
check .expr (nil , &x , s .X )
if x .mode == invalid {
return
}
if !allNumeric (x .typ ) {
check .errorf (s .X , NonNumericIncDec , invalidOp +"%s%s (non-numeric type %s)" , s .X , s .Tok , x .typ )
return
}
Y := &ast .BasicLit {ValuePos : s .X .Pos (), Kind : token .INT , Value : "1" }
check .binary (&x , nil , s .X , Y , op , s .TokPos )
if x .mode == invalid {
return
}
check .assignVar (s .X , nil , &x , "assignment" )
case *ast .AssignStmt :
switch s .Tok {
case token .ASSIGN , token .DEFINE :
if len (s .Lhs ) == 0 {
check .error (s , InvalidSyntaxTree , "missing lhs in assignment" )
return
}
if s .Tok == token .DEFINE {
check .shortVarDecl (inNode (s , s .TokPos ), s .Lhs , s .Rhs )
} else {
check .assignVars (s .Lhs , s .Rhs )
}
default :
if len (s .Lhs ) != 1 || len (s .Rhs ) != 1 {
check .errorf (inNode (s , s .TokPos ), MultiValAssignOp , "assignment operation %s requires single-valued expressions" , s .Tok )
return
}
op := assignOp (s .Tok )
if op == token .ILLEGAL {
check .errorf (atPos (s .TokPos ), InvalidSyntaxTree , "unknown assignment operation %s" , s .Tok )
return
}
var x operand
check .binary (&x , nil , s .Lhs [0 ], s .Rhs [0 ], op , s .TokPos )
if x .mode == invalid {
return
}
check .assignVar (s .Lhs [0 ], nil , &x , "assignment" )
}
case *ast .GoStmt :
check .suspendedCall ("go" , s .Call )
case *ast .DeferStmt :
check .suspendedCall ("defer" , s .Call )
case *ast .ReturnStmt :
res := check .sig .results
if len (s .Results ) == 0 && res .Len () > 0 && res .vars [0 ].name != "" {
for _ , obj := range res .vars {
if alt := check .lookup (obj .name ); alt != nil && alt != obj {
err := check .newError (OutOfScopeResult )
err .addf (s , "result parameter %s not in scope at return" , obj .name )
err .addf (alt , "inner declaration of %s" , obj )
err .report ()
}
}
} else {
var lhs []*Var
if res .Len () > 0 {
lhs = res .vars
}
check .initVars (lhs , s .Results , s )
}
case *ast .BranchStmt :
if s .Label != nil {
check .hasLabel = true
return
}
switch s .Tok {
case token .BREAK :
if ctxt &breakOk == 0 {
check .error (s , MisplacedBreak , "break not in for, switch, or select statement" )
}
case token .CONTINUE :
if ctxt &continueOk == 0 {
check .error (s , MisplacedContinue , "continue not in for statement" )
}
case token .FALLTHROUGH :
if ctxt &fallthroughOk == 0 {
var msg string
switch {
case ctxt &finalSwitchCase != 0 :
msg = "cannot fallthrough final case in switch"
case ctxt &inTypeSwitch != 0 :
msg = "cannot fallthrough in type switch"
default :
msg = "fallthrough statement out of place"
}
check .error (s , MisplacedFallthrough , msg )
}
default :
check .errorf (s , InvalidSyntaxTree , "branch statement: %s" , s .Tok )
}
case *ast .BlockStmt :
check .openScope (s , "block" )
defer check .closeScope ()
check .stmtList (inner , s .List )
case *ast .IfStmt :
check .openScope (s , "if" )
defer check .closeScope ()
check .simpleStmt (s .Init )
var x operand
check .expr (nil , &x , s .Cond )
if x .mode != invalid && !allBoolean (x .typ ) {
check .error (s .Cond , InvalidCond , "non-boolean condition in if statement" )
}
check .stmt (inner , s .Body )
switch s .Else .(type ) {
case nil , *ast .BadStmt :
case *ast .IfStmt , *ast .BlockStmt :
check .stmt (inner , s .Else )
default :
check .error (s .Else , InvalidSyntaxTree , "invalid else branch in if statement" )
}
case *ast .SwitchStmt :
inner |= breakOk
check .openScope (s , "switch" )
defer check .closeScope ()
check .simpleStmt (s .Init )
var x operand
if s .Tag != nil {
check .expr (nil , &x , s .Tag )
check .assignment (&x , nil , "switch expression" )
if x .mode != invalid && !Comparable (x .typ ) && !hasNil (x .typ ) {
check .errorf (&x , InvalidExprSwitch , "cannot switch on %s (%s is not comparable)" , &x , x .typ )
x .mode = invalid
}
} else {
x .mode = constant_
x .typ = Typ [Bool ]
x .val = constant .MakeBool (true )
x .expr = &ast .Ident {NamePos : s .Body .Lbrace , Name : "true" }
}
check .multipleDefaults (s .Body .List )
seen := make (valueMap )
for i , c := range s .Body .List {
clause , _ := c .(*ast .CaseClause )
if clause == nil {
check .error (c , InvalidSyntaxTree , "incorrect expression switch case" )
continue
}
check .caseValues (&x , clause .List , seen )
check .openScope (clause , "case" )
inner := inner
if i +1 < len (s .Body .List ) {
inner |= fallthroughOk
} else {
inner |= finalSwitchCase
}
check .stmtList (inner , clause .Body )
check .closeScope ()
}
case *ast .TypeSwitchStmt :
inner |= breakOk | inTypeSwitch
check .openScope (s , "type switch" )
defer check .closeScope ()
check .simpleStmt (s .Init )
var lhs *ast .Ident
var rhs ast .Expr
switch guard := s .Assign .(type ) {
case *ast .ExprStmt :
rhs = guard .X
case *ast .AssignStmt :
if len (guard .Lhs ) != 1 || guard .Tok != token .DEFINE || len (guard .Rhs ) != 1 {
check .error (s , InvalidSyntaxTree , "incorrect form of type switch guard" )
return
}
lhs , _ = guard .Lhs [0 ].(*ast .Ident )
if lhs == nil {
check .error (s , InvalidSyntaxTree , "incorrect form of type switch guard" )
return
}
if lhs .Name == "_" {
check .softErrorf (lhs , NoNewVar , "no new variable on left side of :=" )
lhs = nil
} else {
check .recordDef (lhs , nil )
}
rhs = guard .Rhs [0 ]
default :
check .error (s , InvalidSyntaxTree , "incorrect form of type switch guard" )
return
}
expr , _ := rhs .(*ast .TypeAssertExpr )
if expr == nil || expr .Type != nil {
check .error (s , InvalidSyntaxTree , "incorrect form of type switch guard" )
return
}
var sx *operand
{
var x operand
check .expr (nil , &x , expr .X )
if x .mode != invalid {
if isTypeParam (x .typ ) {
check .errorf (&x , InvalidTypeSwitch , "cannot use type switch on type parameter value %s" , &x )
} else if IsInterface (x .typ ) {
sx = &x
} else {
check .errorf (&x , InvalidTypeSwitch , "%s is not an interface" , &x )
}
}
}
check .multipleDefaults (s .Body .List )
var lhsVars []*Var
seen := make (map [Type ]ast .Expr )
for _ , s := range s .Body .List {
clause , _ := s .(*ast .CaseClause )
if clause == nil {
check .error (s , InvalidSyntaxTree , "incorrect type switch case" )
continue
}
T := check .caseTypes (sx , clause .List , seen )
check .openScope (clause , "case" )
if lhs != nil {
obj := NewVar (lhs .Pos (), check .pkg , lhs .Name , T )
check .declare (check .scope , nil , obj , clause .Colon )
check .recordImplicit (clause , obj )
lhsVars = append (lhsVars , obj )
}
check .stmtList (inner , clause .Body )
check .closeScope ()
}
if lhs != nil {
var used bool
for _ , v := range lhsVars {
if v .used {
used = true
}
v .used = true
}
if !used {
check .softErrorf (lhs , UnusedVar , "%s declared and not used" , lhs .Name )
}
}
case *ast .SelectStmt :
inner |= breakOk
check .multipleDefaults (s .Body .List )
for _ , s := range s .Body .List {
clause , _ := s .(*ast .CommClause )
if clause == nil {
continue
}
valid := false
var rhs ast .Expr
switch s := clause .Comm .(type ) {
case nil , *ast .SendStmt :
valid = true
case *ast .AssignStmt :
if len (s .Rhs ) == 1 {
rhs = s .Rhs [0 ]
}
case *ast .ExprStmt :
rhs = s .X
}
if rhs != nil {
if x , _ := ast .Unparen (rhs ).(*ast .UnaryExpr ); x != nil && x .Op == token .ARROW {
valid = true
}
}
if !valid {
check .error (clause .Comm , InvalidSelectCase , "select case must be send or receive (possibly with assignment)" )
continue
}
check .openScope (s , "case" )
if clause .Comm != nil {
check .stmt (inner , clause .Comm )
}
check .stmtList (inner , clause .Body )
check .closeScope ()
}
case *ast .ForStmt :
inner |= breakOk | continueOk
check .openScope (s , "for" )
defer check .closeScope ()
check .simpleStmt (s .Init )
if s .Cond != nil {
var x operand
check .expr (nil , &x , s .Cond )
if x .mode != invalid && !allBoolean (x .typ ) {
check .error (s .Cond , InvalidCond , "non-boolean condition in for statement" )
}
}
check .simpleStmt (s .Post )
if s , _ := s .Post .(*ast .AssignStmt ); s != nil && s .Tok == token .DEFINE {
check .softErrorf (s , InvalidPostDecl , "cannot declare in post statement" )
check .use (s .Lhs ...)
}
check .stmt (inner , s .Body )
case *ast .RangeStmt :
inner |= breakOk | continueOk
check .rangeStmt (inner , s )
default :
check .error (s , InvalidSyntaxTree , "invalid statement" )
}
}
func (check *Checker ) rangeStmt (inner stmtContext , s *ast .RangeStmt ) {
type Expr = ast .Expr
type identType = ast .Ident
identName := func (n *identType ) string { return n .Name }
sKey , sValue := s .Key , s .Value
var sExtra ast .Expr = nil
isDef := s .Tok == token .DEFINE
rangeVar := s .X
noNewVarPos := inNode (s , s .TokPos )
var x operand
check .expr (nil , &x , rangeVar )
var key , val Type
if x .mode != invalid {
k , v , cause , ok := rangeKeyVal (x .typ , func (v goVersion ) bool {
return check .allowVersion (v )
})
switch {
case !ok && cause != "" :
check .softErrorf (&x , InvalidRangeExpr , "cannot range over %s: %s" , &x , cause )
case !ok :
check .softErrorf (&x , InvalidRangeExpr , "cannot range over %s" , &x )
case k == nil && sKey != nil :
check .softErrorf (sKey , InvalidIterVar , "range over %s permits no iteration variables" , &x )
case v == nil && sValue != nil :
check .softErrorf (sValue , InvalidIterVar , "range over %s permits only one iteration variable" , &x )
case sExtra != nil :
check .softErrorf (sExtra , InvalidIterVar , "range clause permits at most two iteration variables" )
}
key , val = k , v
}
check .openScope (s , "range" )
defer check .closeScope ()
lhs := [2 ]Expr {sKey , sValue }
rhs := [2 ]Type {key , val }
rangeOverInt := isInteger (x .typ )
if isDef {
var vars []*Var
for i , lhs := range lhs {
if lhs == nil {
continue
}
var obj *Var
if ident , _ := lhs .(*identType ); ident != nil {
name := identName (ident )
obj = NewVar (ident .Pos (), check .pkg , name , nil )
check .recordDef (ident , obj )
if name != "_" {
vars = append (vars , obj )
}
} else {
check .errorf (lhs , InvalidSyntaxTree , "cannot declare %s" , lhs )
obj = NewVar (lhs .Pos (), check .pkg , "_" , nil )
}
assert (obj .typ == nil )
typ := rhs [i ]
if typ == nil || typ == Typ [Invalid ] {
obj .typ = Typ [Invalid ]
obj .used = true
continue
}
if rangeOverInt {
assert (i == 0 )
check .initVar (obj , &x , "range clause" )
} else {
var y operand
y .mode = value
y .expr = lhs
y .typ = typ
check .initVar (obj , &y , "assignment" )
}
assert (obj .typ != nil )
}
if len (vars ) > 0 {
scopePos := s .Body .Pos ()
for _ , obj := range vars {
check .declare (check .scope , nil , obj , scopePos )
}
} else {
check .error (noNewVarPos , NoNewVar , "no new variables on left side of :=" )
}
} else if sKey != nil {
for i , lhs := range lhs {
if lhs == nil {
continue
}
typ := rhs [i ]
if typ == nil || typ == Typ [Invalid ] {
continue
}
if rangeOverInt {
assert (i == 0 )
check .assignVar (lhs , nil , &x , "range clause" )
if x .mode != invalid && !isInteger (x .typ ) {
check .softErrorf (lhs , InvalidRangeExpr , "cannot use iteration variable of type %s" , x .typ )
}
} else {
var y operand
y .mode = value
y .expr = lhs
y .typ = typ
check .assignVar (lhs , nil , &y , "assignment" )
}
}
} else if rangeOverInt {
check .assignment (&x , nil , "range clause" )
}
check .stmt (inner , s .Body )
}
func rangeKeyVal(typ Type , allowVersion func (goVersion ) bool ) (key , val Type , cause string , ok bool ) {
bad := func (cause string ) (Type , Type , string , bool ) {
return Typ [Invalid ], Typ [Invalid ], cause , false
}
orig := typ
switch typ := arrayPtrDeref (coreType (typ )).(type ) {
case nil :
return bad ("no core type" )
case *Basic :
if isString (typ ) {
return Typ [Int ], universeRune , "" , true
}
if isInteger (typ ) {
if allowVersion != nil && !allowVersion (go1_22 ) {
return bad ("requires go1.22 or later" )
}
return orig , nil , "" , true
}
case *Array :
return Typ [Int ], typ .elem , "" , true
case *Slice :
return Typ [Int ], typ .elem , "" , true
case *Map :
return typ .key , typ .elem , "" , true
case *Chan :
if typ .dir == SendOnly {
return bad ("receive from send-only channel" )
}
return typ .elem , nil , "" , true
case *Signature :
if !buildcfg .Experiment .RangeFunc && allowVersion != nil && !allowVersion (go1_23 ) {
return bad ("requires go1.23 or later" )
}
switch {
case typ .Params ().Len () != 1 :
return bad ("func must be func(yield func(...) bool): wrong argument count" )
case typ .Results ().Len () != 0 :
return bad ("func must be func(yield func(...) bool): unexpected results" )
}
assert (typ .Recv () == nil )
cb , _ := coreType (typ .Params ().At (0 ).Type ()).(*Signature )
switch {
case cb == nil :
return bad ("func must be func(yield func(...) bool): argument is not func" )
case cb .Params ().Len () > 2 :
return bad ("func must be func(yield func(...) bool): yield func has too many parameters" )
case cb .Results ().Len () != 1 || !isBoolean (cb .Results ().At (0 ).Type ()):
return bad ("func must be func(yield func(...) bool): yield func does not return bool" )
}
assert (cb .Recv () == nil )
if cb .Params ().Len () >= 1 {
key = cb .Params ().At (0 ).Type ()
}
if cb .Params ().Len () >= 2 {
val = cb .Params ().At (1 ).Type ()
}
return key , val , "" , true
}
return
}
The pages are generated with Golds v0.7.3-preview . (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu .
PR and bug reports are welcome and can be submitted to the issue list .
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds .