关于Go你不得不知道的一些实用小技巧

目录
  • Go 箴言
  • Go 之禅
  • 代码
    • 使用 go fmt 格式化
    • 多个 if 语句可以折叠成 switch
    • 用 chan struct{} 来传递信号, chan bool 表达的不够清楚
    • 30 * time.Second 比 time.Duration(30) * time.Second 更好
    • 用 time.Duration 代替 int64 + 变量名
    • 按类型分组 const 声明,按逻辑和/或类型分组 var
    • 不要在你不拥有的结构上使用 encoding/gob
    • 不要依赖于计算顺序,特别是在 return 语句中。
    • 防止结构体字段用纯值方式初始化,添加 _ struct {} 字段:
    • 为了防止结构比较,添加 func 类型的空字段
    • http.HandlerFunc 比 http.Handler 更好
    • 移动 defer 到顶部
    • JavaScript 解析整数为浮点数并且你的 int64 可能溢出
  • 并发
  • 性能
    • 为了帮助编译器删除绑定检查,请参见此模式 _ = b [7]
  • 构建
  • 测试
  • 工具
  • 其他
  • 总结

Go 箴言

  • 不要通过共享内存进行通信,通过通信共享内存
  • 并发不是并行
  • 管道用于协调;互斥量(锁)用于同步
  • 接口越大,抽象就越弱
  • 利用好零值
  • 空接口 interface{} 没有任何类型约束
  • Gofmt 的风格不是人们最喜欢的,但 gofmt 是每个人的最爱
  • 允许一点点重复比引入一点点依赖更好
  • 系统调用必须始终使用构建标记进行保护
  • 必须始终使用构建标记保护 Cgo
  • Cgo 不是 Go
  • 使用标准库的 unsafe 包,不能保证能如期运行
  • 清晰比聪明更好
  • 反射永远不清晰
  • 错误是值
  • 不要只检查错误,还要优雅地处理它们
  • 设计架构,命名组件,(文档)记录细节
  • 文档是供用户使用的
  • 不要(在生产环境)使用 panic()

Go 之禅

  • 每个 package 实现单一的目的
  • 显式处理错误
  • 尽早返回,而不是使用深嵌套
  • 让调用者处理并发(带来的问题)
  • 在启动一个 goroutine 时,需要知道何时它会停止
  • 避免 package 级别的状态
  • 简单很重要
  • 编写测试以锁定 package API 的行为
  • 如果你觉得慢,先编写 benchmark 来证明
  • 适度是一种美德
  • 可维护性

代码

使用 go fmt 格式化

让团队一起使用官方的 Go 格式工具,不要重新发明轮子。

尝试减少代码复杂度。 这将帮助所有人使代码易于阅读。

多个 if 语句可以折叠成 switch

// NOT BAD
if foo() {
    // ...
} else if bar == baz {
    // ...
} else {
    // ...
}

// BETTER
switch {
case foo():
    // ...
case bar == baz:
    // ...
default:
    // ...
}

用 chan struct{} 来传递信号, chan bool 表达的不够清楚

当你在结构中看到 chan bool 的定义时,有时不容易理解如何使用该值,例如:

type Service struct {
    deleteCh chan bool // what does this bool mean?
}

但是我们可以将其改为明确的 chan struct {} 来使其更清楚:我们不在乎值(它始终是 struct {}),我们关心可能发生的事件,例如:

type Service struct {
    deleteCh chan struct{} // ok, if event than delete something.
}

30 * time.Second 比 time.Duration(30) * time.Second 更好

你不需要将无类型的常量包装成类型,编译器会找出来。

另外最好将常量移到第一位:

// BAD
delay := time.Second * 60 * 24 * 60

// VERY BAD
delay := 60 * time.Second * 60 * 24

// GOOD
delay := 24 * 60 * 60 * time.Second

用 time.Duration 代替 int64 + 变量名

// BAD
var delayMillis int64 = 15000

// GOOD
var delay time.Duration = 15 * time.Second

按类型分组 const 声明,按逻辑和/或类型分组 var

// BAD
const (
    foo = 1
    bar = 2
    message = "warn message"
)

// MOSTLY BAD
const foo = 1
const bar = 2
const message = "warn message"

// GOOD
const (
    foo = 1
    bar = 2
)

const message = "warn message"

这个模式也适用于 var

  • ** 每个阻塞或者 IO 函数操作应该是可取消的或者至少是可超时的
  • ** 为整型常量值实现 Stringer 接口
  • ** 检查 defer 中的错误
  defer func() {
      err := ocp.Close()
      if err != nil {
          rerr = err
      }
  }()
  • ** 不要在 checkErr 函数中使用 panic() 或 os.Exit()
  • ** 仅仅在很特殊情况下才使用 panic, 你必须要去处理 error
  • ** 不要给枚举使用别名,因为这打破了类型安全
  package main
  type Status = int
  type Format = int // remove `=` to have type safety

  const A Status = 1
  const B Format = 1

  func main() {
      println(A == B)
  }
  • **

    如果你想省略返回参数,你最好表示出来

    • _ = f() 比 f() 更好
  • **

    我们用 a := []T{} 来简单初始化 slice

  • **

    用 range 循环来进行数组或 slice 的迭代

    • for _, c := range a[3:7] {...} 比 for i := 3; i < 7; i++ {...} 更好
  • **

    多行字符串用反引号(`)

  • **

    用 _ 来跳过不用的参数

  func f(a int, _ string) {}
  • ** 如果你要比较时间戳,请使用 time.Before 或 time.After ,不要使用 time.Sub 来获得 duration (持续时间),然后检查它的值。
  • ** 带有上下文的函数第一个参数名为 ctx,形如:func foo(ctx Context, ...)
  • ** 几个相同类型的参数定义可以用简短的方式来进行
  func f(a int, b int, s string, p string)
  func f(a, b int, s, p string)
  • ** 一个 slice 的零值是 nil

    var s []int
    fmt.Println(s, len(s), cap(s))
    if s == nil {
    fmt.Println("nil!")
    }
    // Output:
    // [] 0 0
    // nil!
  var a []string
  b := []string{}

  fmt.Println(reflect.DeepEqual(a, []string{}))
  fmt.Println(reflect.DeepEqual(b, []string{}))
  // Output:
  // false
  // true
  • ** 不要将枚举类型与 <><= 和 >= 进行比较

    • 使用确定的值,不要像下面这样做:
  value := reflect.ValueOf(object)
  kind := value.Kind()
  if kind >= reflect.Chan && kind <= reflect.Slice {
    // ...
  }
  • ** 用 %+v 来打印数据的比较全的信息
  • ** 注意空结构 struct{}
  func f1() {
    var a, b struct{}
    print(&a, "\n", &b, "\n") // Prints same address
    fmt.Println(&a == &b)     // Comparison returns false
  }

  func f2() {
    var a, b struct{}
    fmt.Printf("%p\n%p\n", &a, &b) // Again, same address
    fmt.Println(&a == &b)          // ...but the comparison returns true
  }
  • **

    • 例如: errors.Wrap(err, "additional message to a given error")
  • **

    在 Go 里面要小心使用 range:

    • for i := range a and for i, v := range &a ,都不是 a 的副本
    • 但是 for i, v := range a 里面的就是 a 的副本
  • **

    从 map 读取一个不存在的 key 将不会 panic

    • value := map["no_key"] 将得到一个 0 值
    • value, ok := map["no_key"] 更好
  • **

    不要使用原始参数进行文件操作

    • 而不是一个八进制参数 os.MkdirAll(root, 0700)
    • 使用此类型的预定义常量 os.FileMode
  • **

    不要忘记为 iota 指定一种类型

    const (
      _ = iota
      testvar         // testvar 将是 int 类型
    )

vs

    type myType int
    const (
      _ myType = iota
      testvar         // testvar 将是 myType 类型
    )

不要在你不拥有的结构上使用 encoding/gob

在某些时候,结构可能会改变,而你可能会错过这一点。因此,这可能会导致很难找到 bug。

不要依赖于计算顺序,特别是在 return 语句中。

  // BAD
  return res, json.Unmarshal(b, &res)

  // GOOD
  err := json.Unmarshal(b, &res)
  return res, err

防止结构体字段用纯值方式初始化,添加 _ struct {} 字段:

type Point struct {
  X, Y float64
  _    struct{} // to prevent unkeyed literals
}

对于 Point {X:1,Y:1} 都可以,但是对于 Point {1,1} 则会出现编译错误:

./file.go:1:11: too few values in Point literal

当在你所有的结构体中添加了 _ struct{} 后,使用 go vet 命令进行检查,(原来声明的方式)就会提示没有足够的参数。

为了防止结构比较,添加 func 类型的空字段

  type Point struct {
    _ [0]func() // unexported, zero-width non-comparable field
    X, Y float64
  }

http.HandlerFunc 比 http.Handler 更好

用 http.HandlerFunc 你仅需要一个 func,http.Handler 需要一个类型。

移动 defer 到顶部

这可以提高代码可读性并明确函数结束时调用了什么。

JavaScript 解析整数为浮点数并且你的 int64 可能溢出

用 json:"id,string" 代替

type Request struct {
  ID int64 `json:"id,string"`
}

并发

  • ** 以线程安全的方式创建单例(只创建一次)的最好选择是 sync.Once

    • 不要用 flags, mutexes, channels or atomics
  • ** 永远不要使用 select{}, 省略通道, 等待信号
  • ** 不要关闭一个发送(写入)管道,应该由创建者关闭
    • 往一个关闭的 channel 写数据会引起 panic
  • ** math/rand 中的 func NewSource(seed int64) Source 不是并发安全的,默认的 lockedSource 是并发安全的。
  • ** 当你需要一个自定义类型的 atomic 值时,可以使用 atomic.Value

性能

  • ** 不要省略 defer

    • 在大多数情况下 200ns 加速可以忽略不计
  • ** 总是关闭 http body defer r.Body.Close()
    • 除非你需要泄露 goroutine
  • ** 过滤但不分配新内存
  b := a[:0]
  for _, x := range a {
      if f(x) {
        b = append(b, x)
      }
  }

为了帮助编译器删除绑定检查,请参见此模式 _ = b [7]

  • ** time.Time 有指针字段 time.Location 并且这对 go GC 不好

    • 只有使用了大量的 time.Time 才(对性能)有意义,否则用 timestamp 代替
  • ** regexp.MustCompile 比 regexp.Compile 更好
    • 在大多数情况下,你的正则表达式是不可变的,所以你最好在 func init 中初始化它
  • ** 请勿在你的热点代码中过度使用 fmt.Sprintf. 由于维护接口的缓冲池和动态调度,它是很昂贵的。
    • 如果你正在使用 fmt.Sprintf("%s%s", var1, var2), 考虑使用简单的字符串连接。
    • 如果你正在使用 fmt.Sprintf("%x", var), 考虑使用 hex.EncodeToString or strconv.FormatInt(var, 16)
  • ** 如果你不需要用它,可以考虑丢弃它,例如io.Copy(ioutil.Discard, resp.Body)
    • HTTP 客户端的传输不会重用连接,直到body被读完和关闭。
  res, _ := client.Do(req)
  io.Copy(ioutil.Discard, res.Body)
  defer res.Body.Close()
  • ** 不要在循环中使用 defer,否则会导致内存泄露

    • 因为这些 defer 会不断地填满你的栈(内存)
  • ** 不要忘记停止 ticker, 除非你需要泄露 channel
  ticker := time.NewTicker(1 * time.Second)
  defer ticker.Stop()
  • ** 用自定义的 marshaler 去加速 marshaler 过程

    • 但是在使用它之前要进行定制!
  func (entry Entry) MarshalJSON() ([]byte, error) {
    buffer := bytes.NewBufferString("{")
    first := true
    for key, value := range entry {
        jsonValue, err := json.Marshal(value)
        if err != nil {
            return nil, err
        }
        if !first {
            buffer.WriteString(",")
        }
        first = false
        buffer.WriteString(key + ":" + string(jsonValue))
    }
    buffer.WriteString("}")
    return buffer.Bytes(), nil
  }
  • **

    sync.Map 不是万能的,没有很强的理由就不要使用它。

  • **

    在 sync.Pool 中分配内存存储非指针数据

  • **

    为了隐藏逃生分析的指针,你可以小心使用这个函数::

  // noescape hides a pointer from escape analysis.  noescape is
  // the identity function but escape analysis doesn't think the
  // output depends on the input. noescape is inlined and currently
  // compiles down to zero instructions.
  //go:nosplit
  func noescape(p unsafe.Pointer) unsafe.Pointer {
    x := uintptr(p)
    return unsafe.Pointer(x ^ 0)
  }
  • **

    对于最快的原子交换,你可以使用这个 m := (*map[int]int)(atomic.LoadPointer(&ptr))

  • **

    如果执行许多顺序读取或写入操作,请使用缓冲 I/O

    • 减少系统调用次数
  • **

    有 2 种方法清空一个 map:

    • 重用 map 内存 (但是也要注意 m 的回收)
  for k := range m {
    delete(m, k)
  }
  • 分配新的
  m = make(map[int]int)

构建

  • ** 用这个命令 go build -ldflags="-s -w" ... 去掉你的二进制文件
  • ** 拆分构建不同版本的简单方法
    • 用 // +build integration 并且运行他们 go test -v --tags integration .
  • ** 最小的 Go Docker 镜像
  • ** run go format on CI and compare diff
    • 这将确保一切都是生成的和承诺的
  • ** 用最新的 Go 运行 Travis-CI,用 travis 1
  • ** 检查代码格式是否有错误 diff -u <(echo -n) <(gofmt -d .)

测试

  • ** 测试名称 package_test 比 package 要好
  • ** go test -short 允许减少要运行的测试数
  func TestSomething(t *testing.T) {
    if testing.Short() {
      t.Skip("skipping test in short mode.")
    }
  }
  • ** 根据系统架构跳过测试
  if runtime.GOARM == "arm" {
    t.Skip("this doesn't work under ARM")
  }
  • ** 用 testing.AllocsPerRun 跟踪你的内存分配
  • ** 多次运行你的基准测试可以避免噪音。
    • go test -test.bench=. -count=20

工具

  • **

    快速替换 gofmt -w -l -r "panic(err) -> log.Error(err)" .

  • **

    go list 允许找到所有直接和传递的依赖关系

    • go list -f '{{ .Imports }}' package
    • go list -f '{{ .Deps }}' package
  • **

    对于快速基准比较,我们有一个 benchstat 工具。

  • **

    go-critic linter 从这个文件中强制执行几条建议

  • **

    go mod why -m <module> 告诉我们为什么特定的模块在 go.mod 文件中。

  • **

    GOGC=off go build ... 应该会加快构建速度 source

  • **

    内存分析器每 512KB 记录一次分配。你能通过 GODEBUG 环境变量增加比例,来查看你的文件的更多详细信息。

  • **

    go mod why -m <module> 告诉我们为什么特定的模块是在 go.mod 文件中。

其他

  • ** dump goroutines
  go func() {
    sigs := make(chan os.Signal, 1)
    signal.Notify(sigs, syscall.SIGQUIT)
    buf := make([]byte, 1<<20)
    for {
      <-sigs
      stacklen := runtime.Stack(buf, true)
      log.Printf("=== received SIGQUIT ===\n*** goroutine dump...\n%s\n*** end\n"  , buf[:stacklen])
    }
  }()
  • ** 在编译期检查接口的实现

      var _ io.Reader = (*MyFastReader)(nil)
  • ** len(nil) = 0
  • ** 匿名结构很酷
  var hits struct {
    sync.Mutex
    n int
  }
  hits.Lock()
  hits.n++
  hits.Unlock()
  • **

    httputil.DumpRequest 是非常有用的东西,不要自己创建

  • **

    获得调用堆栈,我们可以使用 runtime.Caller

  • **

    要 marshal 任意的 JSON, 你可以 marshal 为 map[string]interface{}{}

  • **

    配置你的 CDPATH 以便你能在任何目录执行 cd github.com/golang/go

    • 添加这一行代码到 bashrc(或者其他类似的) export CDPATH=$CDPATH:$GOPATH/src
  • **

    从一个 slice 生成简单的随机元素

    • []string{"one", "two", "three"}[rand.Intn(3)]

参考资料: github.com/cristaloleg…

总结

到此这篇关于Go你不得不知道的一些实用小技巧的文章就介绍到这了,更多相关Go小技巧内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • golang内置函数len的小技巧

    len是很常用的内置函数,可以测量字符串.slice.array.channel以及map的长度/元素个数. 不过你真的了解len吗?也许还有一些你不知道的小知识. 我们来看一道GO101的题目,这题也被GO语言爱好者周刊转载: package main import "fmt" func main() { var x *struct { s [][32]byte } fmt.Println(len(x.s[99])) } 题目问你这段代码的运行结果,选项有编译错误.panic.32和

  • Golang中使用JSON的一些小技巧分享

    前言 有的时候上游传过来的字段是string类型的,但是我们却想用变成数字来使用. 本来用一个json:",string" 就可以支持了,如果不知道golang的这些小技巧,就要大费周章了. 参考文章: JSON and struct composition in Go 临时忽略struct字段 type User struct { Email string `json:"email"` Password string `json:"password&qu

  • 提升Go语言开发效率的小技巧实例(GO语言语法糖)汇总

    可变长参数 GO语言允许一个函数把任意数量的值作为参数,GO语言内置了**...操作符,在函数的最后一个形参才能使用...**操作符,使用它必须注意如下事项: 可变长参数必须在函数列表的最后一个: 把可变长参数当切片来解析,可变长参数没有没有值时就是nil切片 可变长参数的类型必须相同 func test(a int, b ...int){ return } 既然我们的函数可以接收可变长参数,那么我们在传参的时候也可以传递切片使用**...**进行解包转换为参数列表,append方法就是最好的例

  • Golang中channel使用的一些小技巧

    关闭2次 复制代码 代码如下: ch := make(chan bool) close(ch) close(ch)  // 这样会panic的,channel不能close两次 读取的时候channel提前关闭了 复制代码 代码如下: ch := make(chan string) close(ch) i := <- ch // 不会panic, i读取到的值是空 "",  如果channel是bool的,那么读取到的是false 向已经关闭的channel写数据 复制代码 代码

  • Go本地测试解耦任务拆解及沟通详解

    目录 解耦 no情绪 & todolist 及时沟通 找对人 Go本地测试的思路 我习惯在开发过程中及时测试自己开发的功能模块,这样能及时发现问题,节省后期功能耦合之后,debug的时间. 为了统一管理要测试的功能(模块),所以创建了测试类,在cmd中直接运行,不需要借助postman等接口请求工具. fun Run(){ //测试方法 TestUnifyInputInsert() } func TestUnifyInputInsert() { var req *goods_unify.Good

  • 几个小技巧帮你实现Golang永久阻塞

    目录 使用 sync.WaitGroup 空 select 死循环 用 sync.Mutex os.Signal 空 channel 或者 nil channel 总结 参考 Go 的运行时的当前设计,假定程序员自己负责检测何时终止一个 goroutine 以及何时终止该程序.可以通过调用 os.Exit 或从 main() 函数的返回来以正常方式终止程序.而有时候我们需要的是使程序阻塞在这一行. 使用 sync.WaitGroup 一直等待直到 WaitGroup 等于 0 package m

  • django模型中的字段和model名显示为中文小技巧分享

    简单方法: models.py 复制代码 代码如下: class IceCreamBar(models.Model):     title = models.CharField(max_length=200,db_index=True,verbose_name="名称")         shell = models.CharField(max_length=100,verbose_name='外皮')         filling = models.CharField(max_le

  • django小技巧之html模板中调用对象属性或对象的方法

    环境:依赖最初test2数据库 python3版本            多python版本环境 进入,python3虚拟环境,新建项目test4: ]# cd py3/django-test1/ ]# django-admin startproject test4 创建应用bookshop: ]# cd test4 ]# python manage.py startapp bookshop 修改settings.py主配置文件: ]# vim test4/settings.py ... #数据

  • 关于Go你不得不知道的一些实用小技巧

    目录 Go 箴言 Go 之禅 代码 使用 go fmt 格式化 多个 if 语句可以折叠成 switch 用 chan struct{} 来传递信号, chan bool 表达的不够清楚 30 * time.Second 比 time.Duration(30) * time.Second 更好 用 time.Duration 代替 int64 + 变量名 按类型分组 const 声明,按逻辑和/或类型分组 var 不要在你不拥有的结构上使用 encoding/gob 不要依赖于计算顺序,特别是在

  • 你可能不知道的typescript实用小技巧

    目录 前言 函数重载 映射类型 Partial, Readonly, Nullable, Required Pick, Record Exclude, Omit ReturnType 类型断言 枚举 元组 范型 infer 总结 前言 用了很久的 typescript,用了但感觉又没完全用.因为很多 typescript 的特性没有被使用,查看之前写的代码满屏的 any,这样就容易导致很多 bug,也没有发挥出 typescript 真正的"类型"威力.本文总结了一些使用 typesc

  • 你可能不知道的Vim使用小技巧

    一.用拷贝的内容替换 当发生拼写错误或者想要重命名标识符时,就需要用拷贝的内容来替换当前的名字.比如调用函数时写错了: void letus_fuckit_with_vim(){ cout<<"great!"; } let_fuckat_with_vom(); 只需要先复制上面的函数名,再把光标切换到拼错的词首.然后按下viwp,就替换过来了: void letus_fuckit_with_vim(){ cout<<"great!"; } l

  • Kotlin开发的一些实用小技巧总结

    前言 随着Google I/O大会的召开,Google宣布将支持Kotlin作为Android的开发语言,最近关于Kotlin的文章.介绍就异常的活跃. 本文主要给大家介绍了关于Kotlin开发的一些实用小技巧,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍吧. 1.Lazy Loading(懒加载) 延迟加载有几个好处.延迟加载能让程序启动时间更快,因为加载被推迟到访问变量时. 这在使用 Kotlin 的 Android 应用程序而不是服务器应用程序中特别有用.对于 Androi

  • JavaScript编程的10个实用小技巧

    在这篇文章中,我将列出10个Javascript实用小技巧,主要面向Javascript新手和中级开发者.希望每个读者都能至少从中学到一个有用的技巧. 1.变量转换 看起来很简单,但据我所看到的,使用构造函数,像Array()或者Number()来进行变量转换是常用的做法.始终使用原始数据类型(有时也称为字面量)来转换变量,这种没有任何额外的影响的做法反而效率更高. 复制代码 代码如下: var myVar   = "3.14159",str     = ""+ m

  • 总结MySQL建表、查询优化的一些实用小技巧

    MySQL建表阶段是非常重要的一个环节,表结构的好坏.优劣直接影响着后续的管理维护,赶在明天上班前分享总结个人MySQL建表.MySQL查询优化积累的一些实用小技巧. 技巧一.数据表冗余记录添加时间与更新时间 我们用到的很多数据表大多情况下都会有表记录的"添加时间(add_time)",我建议大家再新增一个记录"更新时间(update_time)"字段,在我的工作里需要为市场部.运营部等建立各种报表,而很多报表里的数据都是需要到大记录表里去查询的,如果直接查询大表的

  • jQuery实用小技巧_输入框文字获取和失去焦点的简单实例

    jQuery实用小技巧_输入框文字获取和失去焦点的简单实例 <input id="txt" class="text1" type="text" /> <script src="js/jquery-1.7.1.min.js"></script> <script type="text/javascript"> $(function () { $("inp

  • 常用的10个Python实用小技巧

    大家好,都说追女孩方法大于态度,学Python也是,今天就给大家分享的是我在用Python编写程序时常用的一些小技巧. 1.多次打印同一个字符 在Python中,不用特地写一个函数来重复打印同一个字符,直接使用Print就可以 tem = 'I Love Python ' print(tem * 3) I Love Python I Love Python I Love Python 2.在函数内部使用生成器 在写Python程序时,我们可以在函数内部直接使用生成器,这样可以使代码更简洁. su

  • pandas参数设置的实用小技巧

    前言 在日常使用pandas的过程中,由于我们所分析的数据表规模.格式上的差异,使得同样的函数或方法作用在不同数据上的效果存在差异. 而pandas有着自己的一套参数设置系统,可以帮助我们在遇到不同的数据时灵活调节从而达到最好的效果,本文就将介绍pandas中常用的参数设置方面的知识. 图1 1 设置DataFrame最大显示行数 pandas设置参数中的display.max_rows用于控制打印出的数据框的最大显示行数,我们使用pd.set_option()来有针对的设置参数,如下面的例子:

  • 学习JavaScript一定要知道的3个小技巧

    目录 一.神奇的扩展运算符 1.拷贝数组 2.合并数组 3.展开对象 二.进行空检查的最佳方法 1.可选链操作符 2.空值合并运算符 三.使用 .map()..reduce() 和 .filter() 前言: 通常在 Angular 或 React 项目中,code review 时看到一些老式的 JavaScript 代码,就会将开发人员归类为初学者.但是,如果您知道以下 3 个技巧,您将被视为现代 JavaScript 的尤达大师.那么,让我们开始我们的旅程吧! 一.神奇的扩展运算符 扩展运

随机推荐