1// Copyright 2011 The Go Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style 3// license that can be found in the LICENSE file. 4 5package main 6 7import ( 8 "fmt" 9 "go/ast" 10 "go/parser" 11 "go/token" 12 "os" 13 "os/exec" 14 "path/filepath" 15 "reflect" 16 "runtime" 17 "strings" 18) 19 20// Partial type checker. 21// 22// The fact that it is partial is very important: the input is 23// an AST and a description of some type information to 24// assume about one or more packages, but not all the 25// packages that the program imports. The checker is 26// expected to do as much as it can with what it has been 27// given. There is not enough information supplied to do 28// a full type check, but the type checker is expected to 29// apply information that can be derived from variable 30// declarations, function and method returns, and type switches 31// as far as it can, so that the caller can still tell the types 32// of expression relevant to a particular fix. 33// 34// TODO(rsc,gri): Replace with go/typechecker. 35// Doing that could be an interesting test case for go/typechecker: 36// the constraints about working with partial information will 37// likely exercise it in interesting ways. The ideal interface would 38// be to pass typecheck a map from importpath to package API text 39// (Go source code), but for now we use data structures (TypeConfig, Type). 40// 41// The strings mostly use gofmt form. 42// 43// A Field or FieldList has as its type a comma-separated list 44// of the types of the fields. For example, the field list 45// x, y, z int 46// has type "int, int, int". 47 48// The prefix "type " is the type of a type. 49// For example, given 50// var x int 51// type T int 52// x's type is "int" but T's type is "type int". 53// mkType inserts the "type " prefix. 54// getType removes it. 55// isType tests for it. 56 57func mkType(t string) string { 58 return "type " + t 59} 60 61func getType(t string) string { 62 if !isType(t) { 63 return "" 64 } 65 return t[len("type "):] 66} 67 68func isType(t string) bool { 69 return strings.HasPrefix(t, "type ") 70} 71 72// TypeConfig describes the universe of relevant types. 73// For ease of creation, the types are all referred to by string 74// name (e.g., "reflect.Value"). TypeByName is the only place 75// where the strings are resolved. 76 77type TypeConfig struct { 78 Type map[string]*Type 79 Var map[string]string 80 Func map[string]string 81 82 // External maps from a name to its type. 83 // It provides additional typings not present in the Go source itself. 84 // For now, the only additional typings are those generated by cgo. 85 External map[string]string 86} 87 88// typeof returns the type of the given name, which may be of 89// the form "x" or "p.X". 90func (cfg *TypeConfig) typeof(name string) string { 91 if cfg.Var != nil { 92 if t := cfg.Var[name]; t != "" { 93 return t 94 } 95 } 96 if cfg.Func != nil { 97 if t := cfg.Func[name]; t != "" { 98 return "func()" + t 99 } 100 } 101 return "" 102} 103 104// Type describes the Fields and Methods of a type. 105// If the field or method cannot be found there, it is next 106// looked for in the Embed list. 107type Type struct { 108 Field map[string]string // map field name to type 109 Method map[string]string // map method name to comma-separated return types (should start with "func ") 110 Embed []string // list of types this type embeds (for extra methods) 111 Def string // definition of named type 112} 113 114// dot returns the type of "typ.name", making its decision 115// using the type information in cfg. 116func (typ *Type) dot(cfg *TypeConfig, name string) string { 117 if typ.Field != nil { 118 if t := typ.Field[name]; t != "" { 119 return t 120 } 121 } 122 if typ.Method != nil { 123 if t := typ.Method[name]; t != "" { 124 return t 125 } 126 } 127 128 for _, e := range typ.Embed { 129 etyp := cfg.Type[e] 130 if etyp != nil { 131 if t := etyp.dot(cfg, name); t != "" { 132 return t 133 } 134 } 135 } 136 137 return "" 138} 139 140// typecheck type checks the AST f assuming the information in cfg. 141// It returns two maps with type information: 142// typeof maps AST nodes to type information in gofmt string form. 143// assign maps type strings to lists of expressions that were assigned 144// to values of another type that were assigned to that type. 145func typecheck(cfg *TypeConfig, f *ast.File) (typeof map[any]string, assign map[string][]any) { 146 typeof = make(map[any]string) 147 assign = make(map[string][]any) 148 cfg1 := &TypeConfig{} 149 *cfg1 = *cfg // make copy so we can add locally 150 copied := false 151 152 // If we import "C", add types of cgo objects. 153 cfg.External = map[string]string{} 154 cfg1.External = cfg.External 155 if imports(f, "C") { 156 // Run cgo on gofmtFile(f) 157 // Parse, extract decls from _cgo_gotypes.go 158 // Map _Ctype_* types to C.* types. 159 err := func() error { 160 txt, err := gofmtFile(f) 161 if err != nil { 162 return err 163 } 164 dir, err := os.MkdirTemp(os.TempDir(), "fix_cgo_typecheck") 165 if err != nil { 166 return err 167 } 168 defer os.RemoveAll(dir) 169 err = os.WriteFile(filepath.Join(dir, "in.go"), txt, 0600) 170 if err != nil { 171 return err 172 } 173 goCmd := "go" 174 if goroot := runtime.GOROOT(); goroot != "" { 175 goCmd = filepath.Join(goroot, "bin", "go") 176 } 177 cmd := exec.Command(goCmd, "tool", "cgo", "-objdir", dir, "-srcdir", dir, "in.go") 178 if reportCgoError != nil { 179 // Since cgo command errors will be reported, also forward the error 180 // output from the command for debugging. 181 cmd.Stderr = os.Stderr 182 } 183 err = cmd.Run() 184 if err != nil { 185 return err 186 } 187 out, err := os.ReadFile(filepath.Join(dir, "_cgo_gotypes.go")) 188 if err != nil { 189 return err 190 } 191 cgo, err := parser.ParseFile(token.NewFileSet(), "cgo.go", out, 0) 192 if err != nil { 193 return err 194 } 195 for _, decl := range cgo.Decls { 196 fn, ok := decl.(*ast.FuncDecl) 197 if !ok { 198 continue 199 } 200 if strings.HasPrefix(fn.Name.Name, "_Cfunc_") { 201 var params, results []string 202 for _, p := range fn.Type.Params.List { 203 t := gofmt(p.Type) 204 t = strings.ReplaceAll(t, "_Ctype_", "C.") 205 params = append(params, t) 206 } 207 for _, r := range fn.Type.Results.List { 208 t := gofmt(r.Type) 209 t = strings.ReplaceAll(t, "_Ctype_", "C.") 210 results = append(results, t) 211 } 212 cfg.External["C."+fn.Name.Name[7:]] = joinFunc(params, results) 213 } 214 } 215 return nil 216 }() 217 if err != nil { 218 if reportCgoError == nil { 219 fmt.Fprintf(os.Stderr, "go fix: warning: no cgo types: %s\n", err) 220 } else { 221 reportCgoError(err) 222 } 223 } 224 } 225 226 // gather function declarations 227 for _, decl := range f.Decls { 228 fn, ok := decl.(*ast.FuncDecl) 229 if !ok { 230 continue 231 } 232 typecheck1(cfg, fn.Type, typeof, assign) 233 t := typeof[fn.Type] 234 if fn.Recv != nil { 235 // The receiver must be a type. 236 rcvr := typeof[fn.Recv] 237 if !isType(rcvr) { 238 if len(fn.Recv.List) != 1 { 239 continue 240 } 241 rcvr = mkType(gofmt(fn.Recv.List[0].Type)) 242 typeof[fn.Recv.List[0].Type] = rcvr 243 } 244 rcvr = getType(rcvr) 245 if rcvr != "" && rcvr[0] == '*' { 246 rcvr = rcvr[1:] 247 } 248 typeof[rcvr+"."+fn.Name.Name] = t 249 } else { 250 if isType(t) { 251 t = getType(t) 252 } else { 253 t = gofmt(fn.Type) 254 } 255 typeof[fn.Name] = t 256 257 // Record typeof[fn.Name.Obj] for future references to fn.Name. 258 typeof[fn.Name.Obj] = t 259 } 260 } 261 262 // gather struct declarations 263 for _, decl := range f.Decls { 264 d, ok := decl.(*ast.GenDecl) 265 if ok { 266 for _, s := range d.Specs { 267 switch s := s.(type) { 268 case *ast.TypeSpec: 269 if cfg1.Type[s.Name.Name] != nil { 270 break 271 } 272 if !copied { 273 copied = true 274 // Copy map lazily: it's time. 275 cfg1.Type = make(map[string]*Type) 276 for k, v := range cfg.Type { 277 cfg1.Type[k] = v 278 } 279 } 280 t := &Type{Field: map[string]string{}} 281 cfg1.Type[s.Name.Name] = t 282 switch st := s.Type.(type) { 283 case *ast.StructType: 284 for _, f := range st.Fields.List { 285 for _, n := range f.Names { 286 t.Field[n.Name] = gofmt(f.Type) 287 } 288 } 289 case *ast.ArrayType, *ast.StarExpr, *ast.MapType: 290 t.Def = gofmt(st) 291 } 292 } 293 } 294 } 295 } 296 297 typecheck1(cfg1, f, typeof, assign) 298 return typeof, assign 299} 300 301// reportCgoError, if non-nil, reports a non-nil error from running the "cgo" 302// tool. (Set to a non-nil hook during testing if cgo is expected to work.) 303var reportCgoError func(err error) 304 305func makeExprList(a []*ast.Ident) []ast.Expr { 306 var b []ast.Expr 307 for _, x := range a { 308 b = append(b, x) 309 } 310 return b 311} 312 313// typecheck1 is the recursive form of typecheck. 314// It is like typecheck but adds to the information in typeof 315// instead of allocating a new map. 316func typecheck1(cfg *TypeConfig, f any, typeof map[any]string, assign map[string][]any) { 317 // set sets the type of n to typ. 318 // If isDecl is true, n is being declared. 319 set := func(n ast.Expr, typ string, isDecl bool) { 320 if typeof[n] != "" || typ == "" { 321 if typeof[n] != typ { 322 assign[typ] = append(assign[typ], n) 323 } 324 return 325 } 326 typeof[n] = typ 327 328 // If we obtained typ from the declaration of x 329 // propagate the type to all the uses. 330 // The !isDecl case is a cheat here, but it makes 331 // up in some cases for not paying attention to 332 // struct fields. The real type checker will be 333 // more accurate so we won't need the cheat. 334 if id, ok := n.(*ast.Ident); ok && id.Obj != nil && (isDecl || typeof[id.Obj] == "") { 335 typeof[id.Obj] = typ 336 } 337 } 338 339 // Type-check an assignment lhs = rhs. 340 // If isDecl is true, this is := so we can update 341 // the types of the objects that lhs refers to. 342 typecheckAssign := func(lhs, rhs []ast.Expr, isDecl bool) { 343 if len(lhs) > 1 && len(rhs) == 1 { 344 if _, ok := rhs[0].(*ast.CallExpr); ok { 345 t := split(typeof[rhs[0]]) 346 // Lists should have same length but may not; pair what can be paired. 347 for i := 0; i < len(lhs) && i < len(t); i++ { 348 set(lhs[i], t[i], isDecl) 349 } 350 return 351 } 352 } 353 if len(lhs) == 1 && len(rhs) == 2 { 354 // x = y, ok 355 rhs = rhs[:1] 356 } else if len(lhs) == 2 && len(rhs) == 1 { 357 // x, ok = y 358 lhs = lhs[:1] 359 } 360 361 // Match as much as we can. 362 for i := 0; i < len(lhs) && i < len(rhs); i++ { 363 x, y := lhs[i], rhs[i] 364 if typeof[y] != "" { 365 set(x, typeof[y], isDecl) 366 } else { 367 set(y, typeof[x], false) 368 } 369 } 370 } 371 372 expand := func(s string) string { 373 typ := cfg.Type[s] 374 if typ != nil && typ.Def != "" { 375 return typ.Def 376 } 377 return s 378 } 379 380 // The main type check is a recursive algorithm implemented 381 // by walkBeforeAfter(n, before, after). 382 // Most of it is bottom-up, but in a few places we need 383 // to know the type of the function we are checking. 384 // The before function records that information on 385 // the curfn stack. 386 var curfn []*ast.FuncType 387 388 before := func(n any) { 389 // push function type on stack 390 switch n := n.(type) { 391 case *ast.FuncDecl: 392 curfn = append(curfn, n.Type) 393 case *ast.FuncLit: 394 curfn = append(curfn, n.Type) 395 } 396 } 397 398 // After is the real type checker. 399 after := func(n any) { 400 if n == nil { 401 return 402 } 403 if false && reflect.TypeOf(n).Kind() == reflect.Pointer { // debugging trace 404 defer func() { 405 if t := typeof[n]; t != "" { 406 pos := fset.Position(n.(ast.Node).Pos()) 407 fmt.Fprintf(os.Stderr, "%s: typeof[%s] = %s\n", pos, gofmt(n), t) 408 } 409 }() 410 } 411 412 switch n := n.(type) { 413 case *ast.FuncDecl, *ast.FuncLit: 414 // pop function type off stack 415 curfn = curfn[:len(curfn)-1] 416 417 case *ast.FuncType: 418 typeof[n] = mkType(joinFunc(split(typeof[n.Params]), split(typeof[n.Results]))) 419 420 case *ast.FieldList: 421 // Field list is concatenation of sub-lists. 422 t := "" 423 for _, field := range n.List { 424 if t != "" { 425 t += ", " 426 } 427 t += typeof[field] 428 } 429 typeof[n] = t 430 431 case *ast.Field: 432 // Field is one instance of the type per name. 433 all := "" 434 t := typeof[n.Type] 435 if !isType(t) { 436 // Create a type, because it is typically *T or *p.T 437 // and we might care about that type. 438 t = mkType(gofmt(n.Type)) 439 typeof[n.Type] = t 440 } 441 t = getType(t) 442 if len(n.Names) == 0 { 443 all = t 444 } else { 445 for _, id := range n.Names { 446 if all != "" { 447 all += ", " 448 } 449 all += t 450 typeof[id.Obj] = t 451 typeof[id] = t 452 } 453 } 454 typeof[n] = all 455 456 case *ast.ValueSpec: 457 // var declaration. Use type if present. 458 if n.Type != nil { 459 t := typeof[n.Type] 460 if !isType(t) { 461 t = mkType(gofmt(n.Type)) 462 typeof[n.Type] = t 463 } 464 t = getType(t) 465 for _, id := range n.Names { 466 set(id, t, true) 467 } 468 } 469 // Now treat same as assignment. 470 typecheckAssign(makeExprList(n.Names), n.Values, true) 471 472 case *ast.AssignStmt: 473 typecheckAssign(n.Lhs, n.Rhs, n.Tok == token.DEFINE) 474 475 case *ast.Ident: 476 // Identifier can take its type from underlying object. 477 if t := typeof[n.Obj]; t != "" { 478 typeof[n] = t 479 } 480 481 case *ast.SelectorExpr: 482 // Field or method. 483 name := n.Sel.Name 484 if t := typeof[n.X]; t != "" { 485 t = strings.TrimPrefix(t, "*") // implicit * 486 if typ := cfg.Type[t]; typ != nil { 487 if t := typ.dot(cfg, name); t != "" { 488 typeof[n] = t 489 return 490 } 491 } 492 tt := typeof[t+"."+name] 493 if isType(tt) { 494 typeof[n] = getType(tt) 495 return 496 } 497 } 498 // Package selector. 499 if x, ok := n.X.(*ast.Ident); ok && x.Obj == nil { 500 str := x.Name + "." + name 501 if cfg.Type[str] != nil { 502 typeof[n] = mkType(str) 503 return 504 } 505 if t := cfg.typeof(x.Name + "." + name); t != "" { 506 typeof[n] = t 507 return 508 } 509 } 510 511 case *ast.CallExpr: 512 // make(T) has type T. 513 if isTopName(n.Fun, "make") && len(n.Args) >= 1 { 514 typeof[n] = gofmt(n.Args[0]) 515 return 516 } 517 // new(T) has type *T 518 if isTopName(n.Fun, "new") && len(n.Args) == 1 { 519 typeof[n] = "*" + gofmt(n.Args[0]) 520 return 521 } 522 // Otherwise, use type of function to determine arguments. 523 t := typeof[n.Fun] 524 if t == "" { 525 t = cfg.External[gofmt(n.Fun)] 526 } 527 in, out := splitFunc(t) 528 if in == nil && out == nil { 529 return 530 } 531 typeof[n] = join(out) 532 for i, arg := range n.Args { 533 if i >= len(in) { 534 break 535 } 536 if typeof[arg] == "" { 537 typeof[arg] = in[i] 538 } 539 } 540 541 case *ast.TypeAssertExpr: 542 // x.(type) has type of x. 543 if n.Type == nil { 544 typeof[n] = typeof[n.X] 545 return 546 } 547 // x.(T) has type T. 548 if t := typeof[n.Type]; isType(t) { 549 typeof[n] = getType(t) 550 } else { 551 typeof[n] = gofmt(n.Type) 552 } 553 554 case *ast.SliceExpr: 555 // x[i:j] has type of x. 556 typeof[n] = typeof[n.X] 557 558 case *ast.IndexExpr: 559 // x[i] has key type of x's type. 560 t := expand(typeof[n.X]) 561 if strings.HasPrefix(t, "[") || strings.HasPrefix(t, "map[") { 562 // Lazy: assume there are no nested [] in the array 563 // length or map key type. 564 if _, elem, ok := strings.Cut(t, "]"); ok { 565 typeof[n] = elem 566 } 567 } 568 569 case *ast.StarExpr: 570 // *x for x of type *T has type T when x is an expr. 571 // We don't use the result when *x is a type, but 572 // compute it anyway. 573 t := expand(typeof[n.X]) 574 if isType(t) { 575 typeof[n] = "type *" + getType(t) 576 } else if strings.HasPrefix(t, "*") { 577 typeof[n] = t[len("*"):] 578 } 579 580 case *ast.UnaryExpr: 581 // &x for x of type T has type *T. 582 t := typeof[n.X] 583 if t != "" && n.Op == token.AND { 584 typeof[n] = "*" + t 585 } 586 587 case *ast.CompositeLit: 588 // T{...} has type T. 589 typeof[n] = gofmt(n.Type) 590 591 // Propagate types down to values used in the composite literal. 592 t := expand(typeof[n]) 593 if strings.HasPrefix(t, "[") { // array or slice 594 // Lazy: assume there are no nested [] in the array length. 595 if _, et, ok := strings.Cut(t, "]"); ok { 596 for _, e := range n.Elts { 597 if kv, ok := e.(*ast.KeyValueExpr); ok { 598 e = kv.Value 599 } 600 if typeof[e] == "" { 601 typeof[e] = et 602 } 603 } 604 } 605 } 606 if strings.HasPrefix(t, "map[") { // map 607 // Lazy: assume there are no nested [] in the map key type. 608 if kt, vt, ok := strings.Cut(t[len("map["):], "]"); ok { 609 for _, e := range n.Elts { 610 if kv, ok := e.(*ast.KeyValueExpr); ok { 611 if typeof[kv.Key] == "" { 612 typeof[kv.Key] = kt 613 } 614 if typeof[kv.Value] == "" { 615 typeof[kv.Value] = vt 616 } 617 } 618 } 619 } 620 } 621 if typ := cfg.Type[t]; typ != nil && len(typ.Field) > 0 { // struct 622 for _, e := range n.Elts { 623 if kv, ok := e.(*ast.KeyValueExpr); ok { 624 if ft := typ.Field[fmt.Sprintf("%s", kv.Key)]; ft != "" { 625 if typeof[kv.Value] == "" { 626 typeof[kv.Value] = ft 627 } 628 } 629 } 630 } 631 } 632 633 case *ast.ParenExpr: 634 // (x) has type of x. 635 typeof[n] = typeof[n.X] 636 637 case *ast.RangeStmt: 638 t := expand(typeof[n.X]) 639 if t == "" { 640 return 641 } 642 var key, value string 643 if t == "string" { 644 key, value = "int", "rune" 645 } else if strings.HasPrefix(t, "[") { 646 key = "int" 647 _, value, _ = strings.Cut(t, "]") 648 } else if strings.HasPrefix(t, "map[") { 649 if k, v, ok := strings.Cut(t[len("map["):], "]"); ok { 650 key, value = k, v 651 } 652 } 653 changed := false 654 if n.Key != nil && key != "" { 655 changed = true 656 set(n.Key, key, n.Tok == token.DEFINE) 657 } 658 if n.Value != nil && value != "" { 659 changed = true 660 set(n.Value, value, n.Tok == token.DEFINE) 661 } 662 // Ugly failure of vision: already type-checked body. 663 // Do it again now that we have that type info. 664 if changed { 665 typecheck1(cfg, n.Body, typeof, assign) 666 } 667 668 case *ast.TypeSwitchStmt: 669 // Type of variable changes for each case in type switch, 670 // but go/parser generates just one variable. 671 // Repeat type check for each case with more precise 672 // type information. 673 as, ok := n.Assign.(*ast.AssignStmt) 674 if !ok { 675 return 676 } 677 varx, ok := as.Lhs[0].(*ast.Ident) 678 if !ok { 679 return 680 } 681 t := typeof[varx] 682 for _, cas := range n.Body.List { 683 cas := cas.(*ast.CaseClause) 684 if len(cas.List) == 1 { 685 // Variable has specific type only when there is 686 // exactly one type in the case list. 687 if tt := typeof[cas.List[0]]; isType(tt) { 688 tt = getType(tt) 689 typeof[varx] = tt 690 typeof[varx.Obj] = tt 691 typecheck1(cfg, cas.Body, typeof, assign) 692 } 693 } 694 } 695 // Restore t. 696 typeof[varx] = t 697 typeof[varx.Obj] = t 698 699 case *ast.ReturnStmt: 700 if len(curfn) == 0 { 701 // Probably can't happen. 702 return 703 } 704 f := curfn[len(curfn)-1] 705 res := n.Results 706 if f.Results != nil { 707 t := split(typeof[f.Results]) 708 for i := 0; i < len(res) && i < len(t); i++ { 709 set(res[i], t[i], false) 710 } 711 } 712 713 case *ast.BinaryExpr: 714 // Propagate types across binary ops that require two args of the same type. 715 switch n.Op { 716 case token.EQL, token.NEQ: // TODO: more cases. This is enough for the cftype fix. 717 if typeof[n.X] != "" && typeof[n.Y] == "" { 718 typeof[n.Y] = typeof[n.X] 719 } 720 if typeof[n.X] == "" && typeof[n.Y] != "" { 721 typeof[n.X] = typeof[n.Y] 722 } 723 } 724 } 725 } 726 walkBeforeAfter(f, before, after) 727} 728 729// Convert between function type strings and lists of types. 730// Using strings makes this a little harder, but it makes 731// a lot of the rest of the code easier. This will all go away 732// when we can use go/typechecker directly. 733 734// splitFunc splits "func(x,y,z) (a,b,c)" into ["x", "y", "z"] and ["a", "b", "c"]. 735func splitFunc(s string) (in, out []string) { 736 if !strings.HasPrefix(s, "func(") { 737 return nil, nil 738 } 739 740 i := len("func(") // index of beginning of 'in' arguments 741 nparen := 0 742 for j := i; j < len(s); j++ { 743 switch s[j] { 744 case '(': 745 nparen++ 746 case ')': 747 nparen-- 748 if nparen < 0 { 749 // found end of parameter list 750 out := strings.TrimSpace(s[j+1:]) 751 if len(out) >= 2 && out[0] == '(' && out[len(out)-1] == ')' { 752 out = out[1 : len(out)-1] 753 } 754 return split(s[i:j]), split(out) 755 } 756 } 757 } 758 return nil, nil 759} 760 761// joinFunc is the inverse of splitFunc. 762func joinFunc(in, out []string) string { 763 outs := "" 764 if len(out) == 1 { 765 outs = " " + out[0] 766 } else if len(out) > 1 { 767 outs = " (" + join(out) + ")" 768 } 769 return "func(" + join(in) + ")" + outs 770} 771 772// split splits "int, float" into ["int", "float"] and splits "" into []. 773func split(s string) []string { 774 out := []string{} 775 i := 0 // current type being scanned is s[i:j]. 776 nparen := 0 777 for j := 0; j < len(s); j++ { 778 switch s[j] { 779 case ' ': 780 if i == j { 781 i++ 782 } 783 case '(': 784 nparen++ 785 case ')': 786 nparen-- 787 if nparen < 0 { 788 // probably can't happen 789 return nil 790 } 791 case ',': 792 if nparen == 0 { 793 if i < j { 794 out = append(out, s[i:j]) 795 } 796 i = j + 1 797 } 798 } 799 } 800 if nparen != 0 { 801 // probably can't happen 802 return nil 803 } 804 if i < len(s) { 805 out = append(out, s[i:]) 806 } 807 return out 808} 809 810// join is the inverse of split. 811func join(x []string) string { 812 return strings.Join(x, ", ") 813} 814