范文健康探索娱乐情感热点
投稿投诉
热点动态
科技财经
情感日志
励志美文
娱乐时尚
游戏搞笑
探索旅游
历史星座
健康养生
美丽育儿
范文作文
教案论文
国学影视

Golang入门到项目实战高效golang开发

  介绍
  Go 是一种新语言。虽然它借鉴了现有语言的思想,但它具有不同寻常的特性,使有效的 Go 程序在特性上不同于用它的亲戚编写的程序。将 C++ 或 Java 程序直接翻译成 Go 不太可能产生令人满意的结果——Java 程序是用 Java 编写的,而不是 Go。另一方面,从 Go 的角度思考问题可能会产生一个成功但完全不同的程序。换句话说,要写好 Go,重要的是要了解它的属性和习语。了解 Go 编程的既定约定也很重要,例如命名、格式化、程序构造等,以便您编写的程序易于其他 Go 程序员理解。
  本文档提供了编写清晰、惯用的 Go 代码的技巧。它扩充了语言规范、Go之旅和如何编写 Go 代码,您应该首先阅读所有这些内容。 格式化
  格式问题是最有争议但最不重要的问题。人们可以适应不同的格式风格,但如果他们没有必要,那就更好了,如果每个人都坚持相同的风格,那么花在该主题上的时间就会更少。问题是如何在没有长篇规范风格指南的情况下接近这个乌托邦。
  在 Go 中,我们采用了一种不同寻常的方法,让机器处理大多数格式问题。该 gofmt 程序(也可用作go fmt ,它在包级别而不是源文件级别运行)读取 Go 程序并以标准的缩进和垂直对齐方式发出源代码,保留并在必要时重新格式化注释。如果你想知道如何处理一些新的布局情况,运行gofmt ; 如果答案似乎不正确,请重新安排您的程序(或提交关于 的错误gofmt ),不要解决它。
  例如,无需花时间排列结构字段的注释。  Gofmt 会为你做的。鉴于声明type T struct {     name string // name of the object     value int // its value }
  gofmt  将排列列:type T struct {     name    string // name of the object     value   int    // its value }
  标准包中的所有 Go 代码都已格式化为 gofmt .
  一些格式细节仍然存在。非常简短: 缩进我们使用制表符进行缩进并 gofmt 默认发出它们。仅在必须时才使用空格。长度限制Go 没有行长度限制。不用担心打孔卡溢出。如果一行感觉太长,请将其包裹起来并用额外的标签缩进。 括弧Go 需要的括号比 C 和 Java 少:控制结构 (  if , for , switch ) 的语法中没有括号。此外,运算符优先级层次结构更短更清晰,因此x<<8 + y<<16 表示间距的含义,与其他语言不同。注释
  Go 提供了 C 风格的 /* */ 块注释和 C++ 风格的// 行注释。行注释是常态;块注释主要作为包注释出现,但在表达式中或禁用大量代码时很有用。
  该程序和 Web 服务器 godoc 处理 Go 源文件以提取有关包内容的文档。出现在顶级声明之前的注释,中间没有换行符,与声明一起被提取出来作为项目的解释性文本。这些注释的性质和风格决定了文档godoc 生成的质量。
  每个包都应该有一个 包注释 ,包子句之前的块注释。对于多文件包,包注释只需要出现在一个文件中,任何一个都可以。包注释应介绍包并提供与整个包相关的信息。它将首先出现在 godoc 页面上,并应设置后面的详细文档。/* Package regexp implements a simple library for regular expressions.  The syntax of the regular expressions accepted is:      regexp:         concatenation { "|" concatenation }     concatenation:         { closure }     closure:         term [ "*" | "+" | "?" ]     term:         "^"         "#39;         "."         character         "[" [ "^" ] character-ranges "]"         "(" regexp ")" */ package regexp
  如果包装简单,包装注释可以简短。 // Package path implements utility routines for // manipulating slash-separated filename paths.
  注释不需要额外的格式,例如星星横幅。生成的输出甚至可能不会以固定宽度的字体呈现,所以不要依赖间距来对齐 godoc ——像 一样gofmt ,会照顾到这一点。注释是未解释的纯文本,因此 HTML 和其他注释(例如)_this_ 将逐字复制,不应使用。一种调整godoc 确实是以固定宽度的字体显示缩进文本,适用于程序片段。对于包注释 fmt 包使用此效果良好。
  根据上下文, godoc 甚至可能不会重新格式化注释,因此请确保它们直接看起来不错:使用正确的拼写、标点符号和句子结构,折叠长行等。
  在包内,紧接在顶级声明之前的任何注释都用作该声明的 文档注释 。程序中每个导出(大写)的名称都应该有一个文档注释。
  文档注释作为完整的句子效果最好,它允许各种自动演示。第一句话应该是一个以声明的名字开头的一句话总结。 // Compile parses a regular expression and returns, if successful, // a Regexp that can be used to match against text. func Compile(str string) (*Regexp, error) {
  如果每个 doc 注释都以其描述的项目名称开头,则可以使用go工具的doc 子命令并通过. 想象一下,你不记得名字"编译",但正在寻找正则表达式的解析函数,所以你运行了命令,  grep $  go doc -all regexp | grep -i parse
  如果包中的所有文档注释都以"此函数..."开头, grep  将无法帮助您记住名称。但是因为包以名称开始每个文档注释,所以您会看到类似这样的内容,它会回忆起您正在寻找的单词。$ go doc -all regexp | grep -i parse     Compile parses a regular expression and returns, if successful, a Regexp     MustCompile is like Compile but panics if the expression cannot be parsed.     parsed. It simplifies safe initialization of global variables holding $
  Go 的声明语法允许对声明进行分组。单个文档注释可以引入一组相关的常量或变量。由于提出了整个声明,这样的注释通常是敷衍的。 // Error codes returned by failures to parse an expression. var (     ErrInternal      = errors.New("regexp: internal error")     ErrUnmatchedLpar = errors.New("regexp: unmatched "("")     ErrUnmatchedRpar = errors.New("regexp: unmatched ")"")     ... )
  分组还可以指示项目之间的关系,例如一组变量受互斥锁保护的事实。 var (     countLock   sync.Mutex     inputCount  uint32     outputCount uint32     errorCount  uint32 ) 命名
  名称在 Go 中与在任何其他语言中一样重要。它们甚至具有语义效果:包外名称的可见性取决于其第一个字符是否为大写。因此,值得花一点时间讨论 Go 程序中的命名约定。
  包名
  导入包时,包名称成为内容的访问器。后 import "bytes"
  导入包可以谈 bytes.Buffer 。如果每个使用包的人都可以使用相同的名称来引用其内容,这将很有帮助,这意味着包名称应该是好的:简短、简洁、令人回味。按照惯例,包使用小写的单字名称;应该不需要下划线或混合大写字母。简而言之,因为使用您的包的每个人都会输入该名称。并且不要担心先验的碰撞。包名只是导入的默认名称;它不需要在所有源代码中都是唯一的,并且在极少数发生冲突的情况下,导入包可以选择不同的名称以在本地使用。在任何情况下,混淆都很少见,因为导入中的文件名决定了正在使用哪个包。
  另一个约定是包名是其源目录的基本名称;中的包 src/encoding/base64  被导入为"encoding/base64" 但具有 name base64 , notencoding_base64 和 not encodingBase64 。
  包的导入器将使用名称来引用其内容,因此包中的导出名称可以使用该事实来避免重复。(不要使用 import . 符号,它可以简化必须在他们正在测试的包之外运行的测试,否则应该避免。)例如,包中的缓冲读取器类型bufio 被称为Reader ,而不是BufReader ,因为用户将其视为bufio.Reader ,这是一个清晰简洁的名称。此外,由于导入的实体总是以其包名寻址,因此bufio.Reader  不会与io.Reader . 类似地,创建新实例的函数ring.Ring ——这是Go 中构造函数的定义——通常会被调用NewRing ,但由于 Ring 是包导出的唯一类型,并且由于包被称为ring ,因此它被称为 just New ,包的客户端将其视为ring.New 。使用包结构来帮助您选择好名字。
  另一个简短的例子是 once.Do ; once.Do(setup) 读得好,不会因写作而改善once.DoOrWaitUntilDone(setup) 。长名称不会自动使内容更具可读性。有用的文档注释通常比超长的名称更有价值。
  Getters
  Go 不提供对 getter 和 setter 的自动支持。自己提供 getter 和 setter 并没有错,而且这样做通常是合适的,但这既不是惯用的,也不是必需 Get 的。如果您有一个名为owner (小写,未导出)的字段 ,则应调用 getter 方法Owner (大写,导出),而不是GetOwner . 使用大写名称导出提供了区分字段和方法的钩子。如果需要,可能会调用 setter 函数SetOwner 。这两个名字在实践中都很好读:owner := obj.Owner() if owner != user {     obj.SetOwner(user) }
  接口名称
  按照惯例,一个方法接口由该方法name加上后缀-er或类似的修改命名构建的试剂名: Reader , Writer ,Formatter , CloseNotifier 等。
  有许多这样的名称,尊重它们和它们捕获的函数名称是很有成效的。  Read ,Write ,Close ,Flush , String 等有规范签名和意义。为避免混淆,除非具有相同的签名和含义,否则不要为您的方法指定其中一个名称。相反,如果您的类型实现了一个与众所周知类型上的方法具有相同含义的方法,则为其赋予相同的名称和签名;调用您的字符串转换器方法String not ToString 。
  混合大写(驼峰命名法)
  最后,Go 中的约定是使用 MixedCaps  ormixedCaps 而不是下划线来编写多词名称。分号
  与 C 一样,Go 的正式语法使用分号来终止语句,但与 C 不同的是,这些分号不会出现在源代码中。相反,词法分析器使用一个简单的规则在扫描时自动插入分号,因此输入文本大部分都没有分号。
  规则是这样的。如果换行符之前的最后一个标记是标识符(包括像 int and 之类的词float64 )、基本文字(例如数字或字符串常量)或标记之一break continue fallthrough return ++ -- ) }
  词法分析器总是在标记后插入一个分号。这可以概括为,"如果换行符出现在可以结束语句的标记之后,则插入分号"。
  分号也可以直接在右大括号之前省略,所以像这样的语句      go func() { for { dst <- <-src } }()
  不需要分号。惯用的 Go 程序仅在 for 循环子句等地方使用分号 ,以分隔初始化器、条件和延续元素。如果您以这种方式编写代码,它们对于分隔一行中的多个语句也是必要的。
  的分号插入规则的一个后果是,你不能把一个控制结构(中左括号 if ,for ,switch ,或select )在下一行。如果这样做,将在大括号之前插入分号,这可能会导致不必要的影响。像这样写它们if i < f() {     g() }
  不是这样 if i < f()  // wrong! {           // wrong!     g() } 控制结构
  Go 的控制结构与 C 的控制结构相关,但在重要方面有所不同。没有 do orwhile 循环,只有一个稍微概括的 for ; switch 更灵活; if 并switch 接受一个可选的初始化语句,如for ; break 和continue 语句采用可选标签来标识要中断或继续的内容;并且有新的控制结构,包括类型开关和多路通信多路复用器,select 。语法也略有不同:没有括号,主体必须始终以大括号分隔。
  如果
  在 Go 中,一个简单的 if 看起来像这样:if x > 0 {     return y }
  强制大括号鼓励 if 在多行上编写简单的语句。无论如何,这样做是一种很好的风格,尤其是当主体包含诸如 areturn 或 之类的控制语句时 break 。
  由于 if 并switch 接受初始化语句,因此通常会看到用于设置局部变量的语句。if err := file.Chmod(0664); err != nil {     log.Print(err)     return err }
  在 Go 库中,你会发现当一个 if 语句没有流入下一个语句时——也就是说,主体以break 、continue 、 goto 、 或return ——结尾,不必要的 else 被省略。f, err := os.Open(name) if err != nil {     return err } codeUsing(f)
  这是代码必须防范一系列错误条件的常见情况的示例。如果成功的控制流沿着页面向下运行,则代码可读性良好,从而消除出现的错误情况。由于错误案例往往以 return  语句结尾,因此生成的代码不需要else 语句。f, err := os.Open(name) if err != nil {     return err } d, err := f.Stat() if err != nil {     f.Close()     return err } codeUsing(f, d)
  声明和赋值
  上一节中的最后一个示例详细说明了  := 简短声明表单的工作原理。调用的声明os.Open 如下,f, err := os.Open(name)
  该语句声明了两个变量, f 和err 。几行之后,调用f.Stat 读取,d, err := f.Stat()
  看起来好像声明了 d and err 。但是请注意,这err 出现在两个语句中。这种重复是合法的:err 由第一条语句声明,但仅在第二条语句中重新赋值。这意味着调用f.Stat 使用err 上面声明的现有 变量,并为其赋予一个新值。
  在 := 声明中,v 即使变量已经声明,也可能出现,条件是:此声明与现有声明的作用域相同 v  (如果v 已在外部作用域中声明,则该声明将创建一个新变量 §),初始化中的相应值可分配给 v ,并且至少有一个由声明创建的其他变量。
  这种不寻常的属性是纯粹的实用主义,可以很容易地使用单个 err 值,例如,在长if-else 链中。你会看到它经常被使用。
  § 这里值得注意的是,在 Go 中,函数参数和返回值的范围与函数体相同,即使它们在词法上出现在包围体的大括号之外。
  for
  Go for 循环与 C 的相似但不相同。它统一for  并且while 没有do-while 。共有三种形式,其中只有一种带有分号。// Like a C for for init; condition; post { }  // Like a C while for condition { }  // Like a C for(;;) for { }
  简短的声明使得在循环中声明索引变量变得容易。 sum := 0 for i := 0; i < 10; i++ {     sum += i }
  如果您在数组、切片、字符串或映射上循环,或者从通道读取, range 子句可以管理循环。for key, value := range oldMap {     newMap[key] = value }
  如果您只需要范围中的第一项(键或索引),请删除第二项: for key := range m {     if key.expired() {         delete(m, key)     } }
  如果您只需要范围中的第二项(值),请使用 空白标识符 (下划线)来丢弃第一项: sum := 0 for _, value := range array {     sum += value }
  空白标识符有很多用途,如后面的部分所述。
  对于字符串, range 它可以为您做更多的工作,通过解析 UTF-8 来分解单个 Unicode 代码点。错误的编码消耗一个字节并产生替换符文 U+FFFD。(名称(带有关联的内置类型)rune 是单个 Unicode 代码点的 Go 术语。有关 详细信息,请参阅语言规范。)循环for pos, char := range "日本 語" { //   is an illegal UTF-8 encoding     fmt.Printf("character %#U starts at byte position %d ", char, pos) }
  打印 character U+65E5 "日" starts at byte position 0 character U+672C "本" starts at byte position 3 character U+FFFD " " starts at byte position 6 character U+8A9E "語" starts at byte position 7
  最后,Go 没有逗号运算符 ++ and--  是语句而不是表达式。因此,如果您想在 a 中运行多个变量,for  您应该使用并行赋值(尽管这排除了++ 和-- )。// Reverse a for i, j := 0, len(a)-1; i < j; i, j = i+1, j-1 {     a[i], a[j] = a[j], a[i] }
  Switch
  Go switch 比 C 更通用。表达式不必是常量甚至整数,情况从上到下计算,直到找到匹配项,如果switch 没有表达式,则打开 true 。因此,将if - else - if -else  链编写 为switch .func unhex(c byte) byte {     switch {     case "0" <= c && c <= "9":         return c - "0"     case "a" <= c && c <= "f":         return c - "a" + 10     case "A" <= c && c <= "F":         return c - "A" + 10     }     return 0 }
  没有自动失败,但案例可以以逗号分隔的列表形式呈现。 func shouldEscape(c byte) bool {     switch c {     case " ", "?", "&", "=", "#", "+", "%":         return true     }     return false }
  尽管它们在 Go 中不像其他一些类似 C 的语言那样常见,但 break 语句可用于提前终止switch 。然而,有时,有必要跳出周围的循环,而不是 switch,而在 Go 中,这可以通过在循环上放置一个标签并"打破"该标签来实现。这个例子展示了这两种用途。Loop: 	for n := 0; n < len(src); n += size { 		switch { 		case src[n] < sizeOne: 			if validateOnly { 				break 			} 			size = 1 			update(src[n])  		case src[n] < sizeTwo: 			if n+1 >= len(src) { 				err = errShortInput 				break Loop 			} 			if validateOnly { 				break 			} 			size = 2 			update(src[n] + src[n+1]< b func Compare(a, b []byte) int {     for i := 0; i < len(a) && i < len(b); i++ {         switch {         case a[i] > b[i]:             return 1         case a[i] < b[i]:             return -1         }     }     switch {     case len(a) > len(b):         return 1     case len(a) < len(b):         return -1     }     return 0 }
  类型switch
  开关还可用于发现接口变量的动态类型。这种 类型开关 使用类型断言的语法, type 在括号内带有关键字。如果 switch 在表达式中声明了一个变量,则该变量将在每个子句中具有相应的类型。在这种情况下重用名称也是惯用的,实际上在每种情况下都声明了一个具有相同名称但类型不同的新变量。var t interface{} t = functionOfSomeType() switch t := t.(type) { default:     fmt.Printf("unexpected type %T ", t)     // %T prints whatever type t has case bool:     fmt.Printf("boolean %t ", t)             // t has type bool case int:     fmt.Printf("integer %d ", t)             // t has type int case *bool:     fmt.Printf("pointer to boolean %t ", *t) // t has type *bool case *int:     fmt.Printf("pointer to integer %d ", *t) // t has type *int } 函数
  多个返回值
  Go 不寻常的特性之一是函数和方法可以返回多个值。这种形式可用于改进 C 程序中的几个笨拙的习惯用法:带内错误返回,例如 -1 forEOF  和修改按地址传递的参数。
  在 C 中,写入错误由负计数表示,错误代码隐藏在易失性位置。在 Go 中, Write  可以返回一个计数和一个错误:"是的,你写了一些字节,但不是全部,因为你填满了设备"。Write 包中文件的方法签名os 是:func (file *File) Write(b []byte) (n int, err error)
  正如文档所说,它返回写入的字节数和非零 error when n  !=  len(b) 。这是一种常见的风格;有关更多示例,请参阅错误处理部分。
  类似的方法不需要传递指向返回值的指针来模拟引用参数。这是一个简单的函数,从字节切片中的某个位置抓取一个数字,返回该数字和下一个位置。 func nextInt(b []byte, i int) (int, int) {     for ; i < len(b) && !isDigit(b[i]); i++ {     }     x := 0     for ; i < len(b) && isDigit(b[i]); i++ {         x = x*10 + int(b[i]) - "0"     }     return x, i }
  您可以使用它来扫描输入切片中的数字, b 如下所示:    for i := 0; i < len(b); {         x, i = nextInt(b, i)         fmt.Println(x)     }
  命名结果参数
  Go 函数的返回或结果"参数"可以命名并用作常规变量,就像传入参数一样。当命名时,它们在函数开始时被初始化为它们的类型的零值;如果函数执行 return 不带参数的语句,则结果参数的当前值将用作返回值。
  这些名称不是强制性的,但它们可以使代码更短、更清晰:它们是文档。如果我们命名 nextInt 它的结果就很明显返回的int  是哪个。func nextInt(b []byte, pos int) (value, nextPos int) {
  因为命名结果被初始化并绑定到一个简单的返回值,它们可以简化和澄清。这是一个 io.ReadFull 很好地使用它们的版本:func ReadFull(r Reader, buf []byte) (n int, err error) {     for len(buf) > 0 && err == nil {         var nr int         nr, err = r.Read(buf)         n += nr         buf = buf[nr:]     }     return }
  defer
  Go 的 defer 语句安排一个函数调用( 延迟函数)在函数执行defer 返回之前立即运行。这是一种不寻常但有效的方法来处理诸如必须释放资源的情况,而不管函数采用哪条路径返回。规范示例是解锁互斥锁或关闭文件。// Contents returns the file"s contents as a string. func Contents(filename string) (string, error) {     f, err := os.Open(filename)     if err != nil {         return "", err     }     defer f.Close()  // f.Close will run when we"re finished.      var result []byte     buf := make([]byte, 100)     for {         n, err := f.Read(buf[0:])         result = append(result, buf[0:n]...) // append is discussed later.         if err != nil {             if err == io.EOF {                 break             }             return "", err  // f will be closed if we return here.         }     }     return string(result), nil // f will be closed if we return here. }
  推迟对诸如此类的函数的调用 Close 有两个优点。首先,它保证您永远不会忘记关闭文件,如果您稍后编辑该函数以添加新的返回路径,则很容易犯这个错误。其次,这意味着收盘价位于开盘价附近,这比将其放在函数的末尾要清晰得多。
  延迟函数的参数(如果函数是方法,则包括接收器)在 延迟  执行时计算,而不是在 调用 执行时计算。除了避免担心在函数执行时变量会改变值,这意味着单个延迟调用站点可以延迟多个函数执行。这是一个愚蠢的例子。 for i := 0; i < 5; i++ {     defer fmt.Printf("%d ", i) }
  延迟函数以 LIFO 顺序执行,因此该代码将导致  4 3 2 1 0 在函数返回时打印。一个更合理的例子是通过程序跟踪函数执行的简单方法。我们可以编写几个简单的跟踪例程,如下所示:func trace(s string)   { fmt.Println("entering:", s) } func untrace(s string) { fmt.Println("leaving:", s) }  // Use them like this: func a() {     trace("a")     defer untrace("a")     // do something.... }
  我们可以通过利用延迟函数的参数在 defer 执行时评估这一事实来做得更好。跟踪例程可以为 untracing 例程设置参数。这个例子:func trace(s string) string {     fmt.Println("entering:", s)     return s }  func un(s string) {     fmt.Println("leaving:", s) }  func a() {     defer un(trace("a"))     fmt.Println("in a") }  func b() {     defer un(trace("b"))     fmt.Println("in b")     a() }  func main() {     b() }
  打印 entering: b in b entering: a in a leaving: a leaving: b
  对于习惯于其他语言的块级资源管理的程序员来说,这 defer 可能看起来很奇怪,但它最有趣和最强大的应用程序恰恰来自于它不是基于块而是基于函数的事实。在 和 的部分中 panic ,recover 我们将看到其可能性的另一个示例。数据
  分配与new
  Go 有两个分配原语,内置函数  new 和make . 它们做不同的事情并适用于不同的类型,这可能会令人困惑,但规则很简单。让我们new 先谈谈。它是一个分配内存的内置函数,但与其他一些语言中的同名函数不同,它不会初始化内存,只会将其归零。也就是说, new(T) 为一个新的 type 项分配零存储 T 并返回它的地址,一个 type 的值*T 。在 Go 术语中,它返回一个指向新分配的类型零值的指针 T 。
  由于由 返回的内存 new 已归零,因此在设计数据结构时安排每种类型的零值无需进一步初始化即可使用是有帮助的。这意味着数据结构的用户可以创建一个new 并开始工作。例如,文档bytes.Buffer 说明"零值Buffer 是一个准备使用的空缓冲区"。同样,sync.Mutex 没有显式构造函数或Init 方法。相反,a 的零值sync.Mutex  被定义为未锁定的互斥锁。
  零值有用的属性可以传递。考虑这个类型声明。 type SyncedBuffer struct {     lock    sync.Mutex     buffer  bytes.Buffer }
  type 的值 SyncedBuffer 也可以在分配或声明后立即使用。在下一个代码段中,p 和v 无需进一步安排即可正常工作。p := new(SyncedBuffer)  // type *SyncedBuffer var v SyncedBuffer      // type  SyncedBuffer
  构造函数和复合字面量
  有时零值不够好,需要一个初始化构造函数,如从 package 派生的这个例子 os 。func NewFile(fd int, name string) *File {     if fd < 0 {         return nil     }     f := new(File)     f.fd = fd     f.name = name     f.dirinfo = nil     f.nepipe = 0     return f }
  里面有很多锅炉板。我们可以使用 复合字面量 来简化它,这是一个每次计算时都会创建一个新实例的表达式。 func NewFile(fd int, name string) *File {     if fd < 0 {         return nil     }     f := File{fd, name, nil, 0}     return &f }
  请注意,与 C 不同,返回局部变量的地址是完全可以的;与变量关联的存储在函数返回后仍然存在。事实上,每次计算复合文字的地址时都会分配一个新实例,因此我们可以将最后两行组合起来。  return &File{fd, name, nil, 0}
  复合文字的字段按顺序排列,并且必须全部存在。但是,通过将元素显式标记为 字段 : 值 对,初始值设定项可以按任何顺序出现,缺失的作为各自的零值保留。因此我们可以说 return &File{fd: fd, name: name}
  作为一种限制情况,如果复合文字根本不包含任何字段,它会为该类型创建一个零值。表达式 new(File) 和&File{} 是等价的。
  还可以为数组、切片和映射创建复合文字,字段标签是索引或映射键(视情况而定)。在这些例子中,初始化工作无论的值的 Enone , Eio 和Einval ,只要它们是不同的。a := [...]string {Enone: "no error", Eio: "Eio", Einval: "invalid argument"} s := []string {Enone: "no error", Eio: "Eio", Einval: "invalid argument"} m := map[int]string{Enone: "no error", Eio: "Eio", Einval: "invalid argument"}
  分配与make
  回到分配。内置函数 make(T, args) 的用途不同于new(T) . 它仅创建切片、映射和通道,并返回类型为(not )的初始化 (未清零)值。区别的原因是这三种类型在幕后表示对必须在使用前初始化的数据结构的引用。例如,切片是一个三项描述符,包含指向数据(在数组内)、长度和容量的指针,并且在这些项被初始化之前,切片是。对于切片、映射和通道, 初始化内部数据结构并准备使用值。例如, T``*T``nil``make make([]int, 10, 100)
  分配一个包含 100 个整数的数组,然后创建一个长度为 10、容量为 100 的切片结构,指向数组的前 10 个元素。(制作切片时,可以省略容量;有关更多信息,请参阅切片部分。)相反, new([]int) 返回指向新分配的、归零的切片结构的指针,即指向nil 切片值的指针。
  这些示例说明了 new 和 之间的区别make 。var p *[]int = new([]int)       // allocates slice structure; *p == nil; rarely useful var v  []int = make([]int, 100) // the slice v now refers to a new array of 100 ints  // Unnecessarily complex: var p *[]int = new([]int) *p = make([]int, 100, 100)  // Idiomatic: v := make([]int, 100)
  请记住,这 make 仅适用于映射、切片和通道,并且不返回指针。获得显式指针分配new 或显式获取变量的地址。
  数组
  数组在规划内存的详细布局时很有用,有时可以帮助避免分配,但主要是它们是切片的构建块,下一节的主题。为了为该主题奠定基础,这里有一些关于数组的词。
  Go 和 C 中数组的工作方式有很大的不同。在 Go 中, 数组是值。将一个数组分配给另一个会复制所有元素。 特别是,如果你将一个数组传递给一个函数,它会收到一个数组的 副本 ,而不是一个指向它的指针。 数组的大小是其类型的一部分。类型 [10]int  和[20]int 是不同的。
  value 属性可能很有用,但也很昂贵;如果你想要类似 C 的行为和效率,你可以传递一个指向数组的指针。 func Sum(a *[3]float64) (sum float64) {     for _, v := range *a {         sum += v     }     return }  array := [...]float64{7.0, 8.5, 9.1} x := Sum(&array)  // Note the explicit address-of operator
  但即使是这种风格也不是惯用的 Go。改用切片。
  切片
  切片包装数组,为数据序列提供更通用、更强大、更方便的接口。除了具有显式维度的项(例如转换矩阵),Go 中的大多数数组编程都是使用切片而不是简单数组完成的。
  切片保存对底层数组的引用,如果将一个切片分配给另一个切片,则两者都引用同一个数组。如果一个函数接受一个切片参数,它对切片元素所做的更改将对调用者可见,类似于传递一个指向底层数组的指针。甲 Read  因此函数可以接受一个切片参数,而不是一个指针和一个计数; 切片内的长度设置了读取数据量的上限。这是package 中类型 的Read 方法的签名 : File``os func (f *File) Read(buf []byte) (n int, err error)
  该方法返回读取的字节数和错误值(如果有)。读入所述第一32个字节的较大的缓冲区的  buf ,切片(这里用作动词)的缓冲液中。n, err := f.Read(buf[0:32])
  这种切片是常见且高效的。事实上,暂时不考虑效率,以下代码段还将读取缓冲区的前 32 个字节。     var n int     var err error     for i := 0; i < 32; i++ {         nbytes, e := f.Read(buf[i:i+1])  // Read one byte.         n += nbytes         if nbytes == 0 || e != nil {             err = e             break         }     }
  切片的长度可以改变,只要它仍然适合底层数组的限制;只需将其分配给自身的一部分。可通过内置函数访问的切片的 容量 cap 报告切片可能采用的最大长度。这是一个将数据附加到切片的函数。如果数据超过容量,则重新分配切片。返回结果切片。该函数使用len 和cap 应用于nil 切片时合法 的事实 ,并返回 0。func Append(slice, data []byte) []byte {     l := len(slice)     if l + len(data) > cap(slice) {  // reallocate         // Allocate double what"s needed, for future growth.         newSlice := make([]byte, (l+len(data))*2)         // The copy function is predeclared and works for any slice type.         copy(newSlice, slice)         slice = newSlice     }     slice = slice[0:l+len(data)]     copy(slice[l:], data)     return slice }
  之后我们必须返回切片,因为虽然 Append  可以修改 的元素slice ,但切片本身(保存指针、长度和容量的运行时数据结构)是按值传递的。
  附加到切片的想法非常有用,它被 append 内置函数捕获 。但是,要了解该函数的设计,我们需要更多信息,因此稍后会返回。
  二维切片
  Go 的数组和切片是一维的。要创建二维数组或切片的等效项,必须定义一个数组数组或切片数组,如下所示: type Transform [3][3]float64  // A 3x3 array, really an array of arrays. type LinesOfText [][]byte     // A slice of byte slices.
  因为切片是可变长度的,所以可以让每个内部切片的长度不同。这可能是一种常见情况,如我们的 LinesOfText  示例所示:每条线都有独立的长度。text := LinesOfText{ 	[]byte("Now is the time"), 	[]byte("for all good gophers"), 	[]byte("to bring some fun to the party."), }
  有时需要分配 2D 切片,例如,在处理像素扫描线时可能会出现这种情况。有两种方法可以实现这一点。一种是独立分配每个slice;另一种是分配单个数组并将各个切片指向其中。使用哪个取决于您的应用程序。如果切片可能会增长或缩小,则应单独分配以避免覆盖下一行;如果没有,使用单个分配构造对象会更有效。作为参考,这里是两种方法的草图。首先,一次一行: // Allocate the top-level slice. picture := make([][]uint8, YSize) // One row per unit of y. // Loop over the rows, allocating the slice for each row. for i := range picture { 	picture[i] = make([]uint8, XSize) }
  现在作为一个分配,分成几行: // Allocate the top-level slice, the same as before. picture := make([][]uint8, YSize) // One row per unit of y. // Allocate one large slice to hold all the pixels. pixels := make([]uint8, XSize*YSize) // Has type []uint8 even though picture is [][]uint8. // Loop over the rows, slicing each row from the front of the remaining pixels slice. for i := range picture { 	picture[i], pixels = pixels[:XSize], pixels[XSize:] }
  Map
  映射是一种方便且强大的内置数据结构,它将一种类型( 键 )的值与另一种类型( 元素 或 值 )的 值相关联 。键可以是定义了相等运算符的任何类型,例如整数、浮点数和复数、字符串、指针、接口(只要动态类型支持相等)、结构和数组。切片不能用作映射键,因为它们没有定义相等性。像切片一样,映射保存对底层数据结构的引用。如果您将地图传递给更改地图内容的函数,则更改将在调用方中可见。
  可以使用带有冒号分隔的键值对的常用复合文字语法构建映射,因此在初始化期间很容易构建它们。 var timeZone = map[string]int{     "UTC":  0*60*60,     "EST": -5*60*60,     "CST": -6*60*60,     "MST": -7*60*60,     "PST": -8*60*60, }
  分配和获取映射值在语法上看起来就像对数组和切片执行相同的操作,只是索引不需要是整数。 offset := timeZone["EST"]
  尝试使用映射中不存在的键获取映射值将返回映射中条目类型的零值。例如,如果映射包含整数,则查找不存在的键将返回 0 。集合可以实现为具有值类型的映射bool 。将映射条目设置true 为将值放入集合中,然后通过简单的索引对其进行测试。attended := map[string]bool{     "Ann": true,     "Joe": true,     ... }  if attended[person] { // will be false if person is not in the map     fmt.Println(person, "was at the meeting") }
  有时您需要将缺失的条目与零值区分开来。是否有条目 "UTC"  或 0 因为它根本不在地图中?你可以用多重赋值的形式来区分。var seconds int var ok bool seconds, ok = timeZone[tz]
  出于显而易见的原因,这被称为"逗号确定"习语。在这个例子中,如果 tz 存在,seconds  将被适当地设置并且ok 为真;如果不是, seconds 将被设置为零并且ok 为假。这是一个将它与一个很好的错误报告放在一起的函数:func offset(tz string) int {     if seconds, ok := timeZone[tz]; ok {         return seconds     }     log.Println("unknown time zone:", tz)     return 0 }
  要在不担心实际值的情况下测试地图中是否存在,您可以使用空白标识符(  _ ) 代替该值的常用变量。_, present := timeZone[tz]
  要删除地图条目,请使用 delete  内置函数,其参数是要删除的地图和键。即使密钥已经不在地图上,这样做也是安全的。delete(timeZone, "PDT")  // Now on Standard Time
  打印
  Go 中的格式化打印使用类似于 C printf  家族的风格,但更丰富和更通用。该函数住在fmt  包装和有大写的名字:fmt.Printf ,fmt.Fprintf , fmt.Sprintf 等。字符串函数(Sprintf 等)返回一个字符串而不是填充提供的缓冲区。
  您不需要提供格式字符串。对于每一个 Printf , Fprintf 和Sprintf 有另一种双功能,如Print 和Println 。这些函数不采用格式字符串,而是为每个参数生成默认格式。这些Println 版本还在参数之间插入一个空格并在输出中附加一个换行符,而这些Print 版本仅在两边的操作数都是字符串时才添加空格。在这个例子中,每一行产生相同的输出。fmt.Printf("Hello %d ", 23) fmt.Fprint(os.Stdout, "Hello ", 23, " ") fmt.Println("Hello", 23) fmt.Println(fmt.Sprint("Hello ", 23))
  格式化打印函数 fmt.Fprint  和友元将任何实现io.Writer 接口的对象作为第一个参数;变量os.Stdout  和os.Stderr 是熟悉的实例。
  在这里,事情开始与 C 不同。首先,数字格式(例如 %d  不带符号或大小的标志);相反,打印例程使用参数的类型来决定这些属性。var x uint64 = 1<<64 - 1 fmt.Printf("%d %x; %d %x ", x, x, int64(x), int64(x))
  打印 18446744073709551615 ffffffffffffffff; -1 -1
  如果你只想要默认的转换,比如整数的十进制,你可以使用笼统的格式 %v (对于"值");结果是什么Print ,并Println 会产生。此外,该格式可以打印任何值,甚至是数组、切片、结构和映射。这是上一节中定义的时区地图的打印语句。fmt.Printf("%v ", timeZone)  // or just fmt.Println(timeZone)
  这给出了输出: map[CST:-21600 EST:-18000 MST:-25200 PST:-28800 UTC:0]
  对于地图, Printf 朋友按字典顺序对输出进行排序。
  打印结构体时,修改后的格式会 %+v 用名称注释结构体的字段,对于任何值,替代格式会%#v 以完整的 Go 语法打印值。type T struct {     a int     b float64     c string } t := &T{ 7, -2.35, "abc	def" } fmt.Printf("%v ", t) fmt.Printf("%+v ", t) fmt.Printf("%#v ", t) fmt.Printf("%#v ", timeZone)
  打印 &{7 -2.35 abc def} &{a:7 b:-2.35 c:abc def} &main.T{a:7, b:-2.35, c:"abc	def"} map[string]int{"CST":-21600, "EST":-18000, "MST":-25200, "PST":-28800, "UTC":0}
  (注意&符号。) %q 当应用于类型为string 或的值时,也可以使用带引号的字符串格式[]byte 。%#q 如果可能,替代格式将使用反引号代替。(该%q 格式也适用于整数和符文,生成单引号符文常量。)此外,%x 适用于字符串、字节数组和字节切片以及整数,生成一个长的十六进制字符串,并在格式中使用空格(% x ) 它在字节之间放置空格。
  另一种方便的格式是 %T ,它打印值的类型。fmt.Printf("%T ", timeZone)
  打印 map[string]int
  如果要控制自定义类型的默认格式,只需定义一个带有 String() string 类型签名的方法。对于我们的简单类型T ,可能看起来像这样。func (t *T) String() string {     return fmt.Sprintf("%d/%g/%q", t.a, t.b, t.c) } fmt.Printf("%v ", t)
  按格式打印 7/-2.35/"abc	def"
  (如果您需要打印类型的 值 T 以及指向 的指针T ,则 for 的接收器String 必须是值类型;此示例使用指针,因为这对于结构类型更有效和更惯用。请参阅下面关于指针与值接收器的部分以了解更多信息。)
  我们的 String 方法能够调用,Sprintf 因为打印例程是完全可重入的并且可以用这种方式包装。然而,关于这种方法有一个重要的细节需要理解:不要String 通过调用Sprintf 的方式来构造一个方法 ,这种方式会String  无限期地重复出现在你的方法中。如果Sprintf  调用尝试将接收器直接打印为字符串,则可能会发生这种情况,而后者又会再次调用该方法。如本示例所示,这是一个常见且容易犯的错误。type MyString string  func (m MyString) String() string {     return fmt.Sprintf("MyString=%s", m) // Error: will recur forever. }
  修复也很容易:将参数转换为基本字符串类型,它没有方法。 type MyString string func (m MyString) String() string {     return fmt.Sprintf("MyString=%s", string(m)) // OK: note conversion. }
  在初始化部分,我们将看到另一种避免这种递归的技术。
  另一种打印技术是将打印例程的参数直接传递给另一个这样的例程。的签名 Printf 使用...interface{}  其最终参数的类型来指定任意数量的参数(任意类型)可以出现在格式之后。func Printf(format string, v ...interface{}) (n int, err error) {
  在函数内 Printf ,v 就像一个类型的变量, []interface{} 但如果它被传递给另一个可变参数函数,它就像一个常规的参数列表。这是log.Println 我们上面使用的函数的实现。它将其参数直接传递 fmt.Sprintln 给实际格式。// Println prints to the standard logger in the manner of fmt.Println. func Println(v ...interface{}) {     std.Output(2, fmt.Sprintln(v...))  // Output takes parameters (int, string) }
  我们在嵌套调用中写 ... after告诉编译器将其视为参数列表;否则它只会作为单个切片参数传递 。 v``Sprintln``v``v
  打印的内容比我们在这里介绍的还要多。有关详细信息,请参阅 godoc 包的文档fmt 。
  顺便说一句, ... 参数可以是特定类型的,例如...int  对于选择整数列表中最小者的 min 函数:func Min(a ...int) int {     min := int(^uint(0) >> 1)  // largest int     for _, i := range a {         if i < min {             min = i         }     }     return min }
  追加
  现在我们有了解释 append 内置函数设计所需的缺失部分。的签名append  与我们Append 上面的自定义函数不同。示意图是这样的:func append(slice [] T , elements ... T ) [] T
  其中 T 是任何给定类型的占位符。您实际上无法在 Go 中编写类型 T  由调用者确定的函数。这append 就是内置的原因:它需要编译器的支持。
  什么 append 是将元素附加到切片的末尾并返回结果。结果需要返回,因为与我们的手写一样Append ,底层数组可能会改变。这个简单的例子x := []int{1,2,3} x = 追加(x, 4, 5, 6) fmt.Println(x)
  打印 [1 2 3 4 5 6] 。所以append 有点像Printf ,收集任意数量的参数。
  但是如果我们想做我们 Append 所做的并将切片附加到切片怎么办?简单:... 在调用站点使用,就像我们在Output 上面调用中所做的那样。这个片段产生与上面相同的输出。x := []int{1,2,3} x = append(x, 4, 5, 6) fmt.Println(x)
  没有那个 ... ,它就不会编译,因为类型是错误的;y 不是 类型int 。初始化
  虽然从表面上看它与 C 或 C++ 中的初始化没有太大区别,但 Go 中的初始化功能更强大。可以在初始化期间构建复杂的结构,并且可以正确处理初始化对象之间,甚至不同包之间的排序问题。
  常数
  Go 中的常量就是——常量。它们是在编译时创建的,即使在函数中定义为局部变量,并且只能是数字、字符(符文)、字符串或布尔值。由于编译时限制,定义它们的表达式必须是可由编译器计算的常量表达式。例如,  1<<3 是一个常量表达式,而 math.Sin(math.Pi/4) 不是因为函数调用math.Sin 需要在运行时发生。
  在 Go 中,枚举常量是使用 iota  枚举器创建的。由于iota 可以是表达式的一部分并且表达式可以隐式重复,因此很容易构建复杂的值集。type ByteSize float64  const (     _           = iota // ignore first value by assigning to blank identifier     KB ByteSize = 1 << (10 * iota)     MB     GB     TB     PB     EB     ZB     YB )
  将方法附加 String 到任何用户定义的类型的能力使得任意值可以自动格式化以进行打印。尽管您会看到它最常应用于结构,但此技术对于标量类型也很有用,例如像ByteSize .func (b ByteSize) String() string {     switch {     case b >= YB:         return fmt.Sprintf("%.2fYB", b/YB)     case b >= ZB:         return fmt.Sprintf("%.2fZB", b/ZB)     case b >= EB:         return fmt.Sprintf("%.2fEB", b/EB)     case b >= PB:         return fmt.Sprintf("%.2fPB", b/PB)     case b >= TB:         return fmt.Sprintf("%.2fTB", b/TB)     case b >= GB:         return fmt.Sprintf("%.2fGB", b/GB)     case b >= MB:         return fmt.Sprintf("%.2fMB", b/MB)     case b >= KB:         return fmt.Sprintf("%.2fKB", b/KB)     }     return fmt.Sprintf("%.2fB", b) }
  表达式 YB 打印为1.00YB ,而ByteSize(1e13) 打印为9.09TB 。
  这里使用 Sprintf  来实现ByteSize 的String 方法是安全的(避免无限重复)不是因为转换而是因为它调用Sprintf with %f ,它不是字符串格式:Sprintf 只会在String 需要字符串时调用该方法,并且%f  需要浮动 -点值。
  变量
  变量可以像常量一样初始化,但初始化器可以是在运行时计算的通用表达式。 var (     home   = os.Getenv("HOME")     user   = os.Getenv("USER")     gopath = os.Getenv("GOPATH") )
  初始化函数
  最后,每个源文件都可以定义自己的 niladic init 函数来设置所需的任何状态。(实际上每个文件可以有多个 init 函数。) finally 的意思是 finally:init 在包中的所有变量声明都评估了它们的初始值设定项之后调用,并且只有在所有导入的包都已初始化之后才评估它们。
  除了不能表示为声明的初始化之外, init 函数的一个常见用途是在真正执行开始之前验证或修复程序状态的正确性。func init() {     if user == "" {         log.Fatal("$USER not set")     }     if home == "" {         home = "/home/" + user     }     if gopath == "" {         gopath = home + "/go"     }     // gopath may be overridden by --gopath flag on command line.     flag.StringVar(&gopath, "gopath", gopath, "override default GOPATH") } 方法
  指针与值
  正如我们在 中看到的 ByteSize ,可以为任何命名类型定义方法(指针或接口除外);接收者不必是一个结构体。
  在上面对切片的讨论中,我们编写了一个 Append  函数。我们可以将其定义为切片上的方法。为此,我们首先声明一个可以绑定方法的命名类型,然后使方法的接收器成为该类型的值。type ByteSlice []byte  func (slice ByteSlice) Append(data []byte) []byte {     // Body exactly the same as the Append function defined above. }
  这仍然需要返回更新切片的方法。我们可以通过重新定义方法采取消除笨拙  指针 到 ByteSlice 它的接收器,因此该方法可以覆盖调用者的切片。func (p *ByteSlice) Append(data []byte) {     slice := *p     // Body as above, without the return.     *p = slice }
  事实上,我们还可以做得更好。如果我们修改我们的函数,让它看起来像一个标准的 Write 方法,像这样,func (p *ByteSlice) Write(data []byte) (n int, err error) {     slice := *p     // Again as above.     *p = slice     return len(data), nil }
  那么类型 *ByteSlice 满足标准接口 io.Writer ,就方便了。例如,我们可以打印成一个。    var b ByteSlice     fmt.Fprintf(&b, "This hour has %d days ", 7)
  我们传递 a 的地址, ByteSlice  因为只*ByteSlice 满足io.Writer 。关于接收者的指针与值的规则是值方法可以在指针和值上调用,但指针方法只能在指针上调用。
  出现这个规则是因为指针方法可以修改接收者;在一个值上调用它们将导致该方法接收该值的副本,因此任何修改都将被丢弃。因此,该语言不允许这种错误。但是,有一个方便的例外。当值可寻址时,该语言会通过自动插入地址运算符来处理对值调用指针方法的常见情况。在我们的例子中,变量 b 是可寻址的,所以我们可以Write 只用b.Write . 编译器会(&b).Write 为我们重写它。
  顺便说一句,在 Write 字节切片上使用的想法是bytes.Buffer .接口和其他类型
  接口
  Go 中的接口提供了一种指定对象行为的方法:如果某些东西可以做到 这一点 ,那么它就可以在 这里 使用 。我们已经看到了几个简单的例子;自定义打印机可以通过 String 方法实现,同时Fprintf 可以通过方法生成任何内容的输出Write 。只有一个或两个方法的接口在 Go 代码中很常见,并且通常被赋予一个从方法派生的名称,例如io.Writer  实现Write .
  一个类型可以实现多个接口。例如,一个集合可以通过在包中的例程进行排序 sort ,如果它实现了 sort.Interface ,其中包含Len() , Less(i, j int) bool 以及Swap(i, j int) ,它也可以有一个自定义的格式。在这个人为的例子中,Sequence 两者都满足。type Sequence []int  // Methods required by sort.Interface. func (s Sequence) Len() int {     return len(s) } func (s Sequence) Less(i, j int) bool {     return s[i] < s[j] } func (s Sequence) Swap(i, j int) {     s[i], s[j] = s[j], s[i] }  // Copy returns a copy of the Sequence. func (s Sequence) Copy() Sequence {     copy := make(Sequence, 0, len(s))     return append(copy, s...) }  // Method for printing - sorts the elements before printing. func (s Sequence) String() string {     s = s.Copy() // Make a copy; don"t overwrite argument.     sort.Sort(s)     str := "["     for i, elem := range s { // Loop is O(N²); will fix that in next example.         if i > 0 {             str += " "         }         str += fmt.Sprint(elem)     }     return str + "]" }
  转化次数
  的 String 方法Sequence 是重新创建Sprint 已经为切片所做的工作。(它也有复杂度 O(N²),这很差。)如果我们在调用 之前将 转换Sequence 为普通的 []int ,我们可以分担(并加快速度)Sprint 。func (s Sequence) String() string {     s = s.Copy()     sort.Sort(s)     return fmt.Sprint([]int(s)) }
  此方法是 Sprintf 从String 方法安全调用的转换技术的另一个示例 。因为如果我们忽略类型名称,这两种类型(Sequence 和[]int )是相同的,所以在它们之间进行转换是合法的。转换不会创建新值,它只是暂时充当现有值好像具有新类型一样。(还有其他合法的转换,例如从整数到浮点数,确实会创建一个新值。)
  Go 程序中的一个习惯用法是转换表达式的类型以访问不同的方法集。例如,我们可以使用现有类型 sort.IntSlice 将整个示例简化为:,我们不再Sequence 实现多个接口(排序和打印),而是使用将数据项转换为多种类型(Sequence ,sort.IntSlice  和[]int )的能力,每种类型都完成部分工作。这在实践中更不寻常,但可能有效。type Sequence []int  // Method for printing - sorts the elements before printing func (s Sequence) String() string {     s = s.Copy()     sort.IntSlice(s).Sort()     return fmt.Sprint([]int(s)) }
  接口转换和类型断言
  类型开关是一种转换形式:它们采用一个接口,对于开关中的每个 case,在某种意义上将其转换为那个 case 的类型。下面是代码如何 fmt.Printf 使用类型开关将值转换为字符串的简化版本。如果它已经是一个字符串,我们想要接口保存的实际字符串值,而如果它有一个 String 方法,我们想要调用该方法的结果。type Stringer interface {     String() string }  var value interface{} // Value provided by caller. switch str := value.(type) { case string:     return str case Stringer:     return str.String() }
  第一种情况找到了一个具体的值;第二个将接口转换为另一个接口。以这种方式混合类型非常好。
  如果我们只关心一种类型怎么办?如果我们知道该值包含 a string  而我们只想提取它?one-case 类型 switch 可以,但类型 assertion 也可以。类型断言采用接口值并从中提取指定显式类型的值。语法借用了打开类型开关的子句,但使用显式类型而不是type 关键字:value.(typeName)
  结果是一个静态类型的新值 typeName 。该类型必须是接口持有的具体类型,或者值可以转换为的第二个接口类型。要提取我们知道在值中的字符串,我们可以这样写:str := value.(string)
  但如果结果证明该值不包含字符串,则程序将因运行时错误而崩溃。为了防止出现这种情况,请使用"逗号,好的"习语来安全地测试该值是否为字符串: str, ok := value.(string) if ok {     fmt.Printf("string value is: %q ", str) } else {     fmt.Printf("value is not a string ") }
  如果类型断言失败, str 它将仍然存在并且是字符串类型,但它将具有零值,一个空字符串。
  作为功能的说明,这里有一个 if -else  语句,它等效于打开此部分的类型开关。if str, ok := value.(string); ok {     return str } else if str, ok := value.(Stringer); ok {     return str.String() }
  概论
  如果一个类型只是为了实现一个接口而存在,并且永远不会在该接口之外导出方法,则不需要导出该类型本身。仅导出接口可以清楚地表明该值除了接口中描述的之外没有其他有趣的行为。它还避免了对通用方法的每个实例重复文档的需要。
  在这种情况下,构造函数应该返回一个接口值而不是实现类型。例如,在哈希库中, crc32.NewIEEE 和 都adler32.New  返回接口类型hash.Hash32 。在 Go 程序中用 CRC-32 算法代替 Adler-32 只需要改变构造函数调用;其余代码不受算法变化的影响。
  类似的方法允许将各种 crypto 包中的流密码算法与它们链接在一起的分组密码分开。包中的Block 接口crypto/cipher 指定块密码的行为,它提供单个数据块的加密。然后,通过与bufio 包的类比,实现该接口的密码包可用于构造流密码,由该Stream 接口表示,而无需了解块加密的细节。
  该  crypto/cipher 接口是这样的:type Block interface {     BlockSize() int     Encrypt(dst, src []byte)     Decrypt(dst, src []byte) }  type Stream interface {     XORKeyStream(dst, src []byte) }
  这是计数器模式 (CTR) 流的定义,它将块密码转换为流密码;请注意,块密码的详细信息被抽象掉了: // NewCTR returns a Stream that encrypts/decrypts using the given Block in // counter mode. The length of iv must be the same as the Block"s block size. func NewCTR(block Block, iv []byte) Stream
  NewCTR 不仅适用于一种特定的加密算法和数据源,还适用于Block 接口的任何实现和任何 Stream . 因为它们返回接口值,所以用其他加密模式替换 CTR 加密是一种本地化的变化。必须编辑构造函数调用,但由于周围的代码必须仅将结果视为Stream ,因此不会注意到差异。
  接口和方法
  由于几乎任何东西都可以附加方法,因此几乎任何东西都可以满足接口。一个说明性的例子是在 http  包中,它定义了Handler 接口。任何实现的对象Handler 都可以为 HTTP 请求提供服务。type Handler interface {     ServeHTTP(ResponseWriter, *Request) }
  ResponseWriter 本身是一个接口,它提供对将响应返回给客户端所需的方法的访问。这些方法包括标准Write 方法,因此 http.ResponseWriter 可以在任何可以使用的地方使用 an io.Writer  。 Request 是一个包含来自客户端的请求的解析表示的结构。
  为简洁起见,让我们忽略 POST 并假设 HTTP 请求总是 GET;这种简化不会影响处理程序的设置方式。这是一个处理程序的简单实现,用于计算页面被访问的次数。 // Simple counter server. type Counter struct {     n int }  func (ctr *Counter) ServeHTTP(w http.ResponseWriter, req *http.Request) {     ctr.n++     fmt.Fprintf(w, "counter = %d ", ctr.n) }
  (与我们的主题保持一致,注意如何 Fprintf 打印到 http.ResponseWriter .)在真实服务器中,访问ctr.n 需要防止并发访问。有关建议,请参阅sync 和atomic 包。
  作为参考,这里是如何将这样的服务器附加到 URL 树上的节点。 import "net/http" ... ctr := new(Counter) http.Handle("/counter", ctr)
  但是为什么要创建 Counter 一个结构体呢?一个整数就是所需要的。(接收者需要是一个指针,以便调用者可以看到增量。)// Simpler counter server. type Counter int  func (ctr *Counter) ServeHTTP(w http.ResponseWriter, req *http.Request) {     *ctr++     fmt.Fprintf(w, "counter = %d ", *ctr) }
  如果您的程序有一些需要通知页面已被访问的内部状态怎么办?将频道绑定到网页。 // A channel that sends a notification on each visit. // (Probably want the channel to be buffered.) type Chan chan *http.Request  func (ch Chan) ServeHTTP(w http.ResponseWriter, req *http.Request) {     ch <- req     fmt.Fprint(w, "notification sent") }
  最后,假设我们想显示 /args 调用服务器二进制文件时使用的参数。编写一个函数来打印参数很容易。func ArgServer() {     fmt.Println(os.Args) }
  我们如何将其转换为 HTTP 服务器?我们可以创建 ArgServer  一个我们忽略其值的某种类型的方法,但有一种更简洁的方法。由于我们可以为除指针和接口之外的任何类型定义方法,因此我们可以为函数编写方法。该http 包包含以下代码:// The HandlerFunc type is an adapter to allow the use of // ordinary functions as HTTP handlers.  If f is a function // with the appropriate signature, HandlerFunc(f) is a // Handler object that calls f. type HandlerFunc func(ResponseWriter, *Request)  // ServeHTTP calls f(w, req). func (f HandlerFunc) ServeHTTP(w ResponseWriter, req *Request) {     f(w, req) }
  HandlerFunc 是带有方法的类型ServeHTTP ,因此该类型的值可以为 HTTP 请求提供服务。看方法的实现:接收者是一个函数,f ,方法调用f 。这可能看起来很奇怪,但它与接收者是一个通道和在通道上发送的方法并没有什么不同。
  为了做成 ArgServer 一个 HTTP 服务器,我们首先修改它以获得正确的签名。// Argument server. func ArgServer(w http.ResponseWriter, req *http.Request) {     fmt.Fprintln(w, os.Args) }
  ArgServer 现在有相同的签名HandlerFunc ,因此它可以被转换成该类型来访问它的方法,就像我们转换Sequence 到IntSlice  访问IntSlice.Sort 。设置它的代码很简洁:http.Handle("/args", http.HandlerFunc(ArgServer))
  当有人访问该页面时 /args ,安装在该页面上的处理程序具有值ArgServer  和类型HandlerFunc 。HTTP 服务器将调用该ServeHTTP  类型的方法,ArgServer 作为接收方,接收方将依次调用 ArgServer (通过f(w, req)  内部调用HandlerFunc.ServeHTTP )。然后将显示参数。
  在本节中,我们从一个结构体、一个整数、一个通道和一个函数创建了一个 HTTP 服务器,所有这一切都是因为接口只是一组方法,可以为(几乎)任何类型定义。 空白标识符
  我们已经在 for  range loops 和maps上下文中多次提到了空白标识符 。可以使用任何类型的任何值分配或声明空白标识符,并无害地丢弃该值。这有点像写入 Unix/dev/null 文件:它代表一个只写值,用作占位符,其中需要变量但实际值无关紧要。它的用途超出了我们已经见过的用途。
  多重赋值中的空白标识符
  在 for  range 循环中使用空白标识符是一般情况的特例:多重赋值。
  如果赋值需要左侧的多个值,但其中一个值不会被程序使用,则赋值左侧的空白标识符可避免创建虚拟变量的需要,并明确表示该值将被丢弃。例如,当调用一个函数返回一个值和一个错误,但只有错误是重要的时,使用空白标识符来丢弃不相关的值。 if _, err := os.Stat(path); os.IsNotExist(err) { 	fmt.Printf("%s does not exist ", path) }
  有时,您会看到为了忽略错误而丢弃错误值的代码;这是可怕的做法。始终检查错误返回;提供它们是有原因的。 // Bad! This code will crash if path does not exist. fi, _ := os.Stat(path) if fi.IsDir() {     fmt.Printf("%s is a directory ", path) }
  未使用的导入和变量
  导入包或声明变量而不使用它是错误的。未使用的导入会使程序膨胀并且编译速度变慢,而初始化但未使用的变量至少是一种浪费的计算,并且可能表明存在更大的错误。然而,当程序处于积极开发状态时,经常会出现未使用的导入和变量,删除它们只是为了让编译继续进行,只是为了以后再次需要它们会很烦人。空白标识符提供了一种解决方法。
  这个写了一半的程序有两个未使用的导入( fmt 和io )和一个未使用的变量(fd ),所以它不会编译,但最好看看到目前为止的代码是否正确。package main  import (     "fmt"     "io"     "log"     "os" )  func main() {     fd, err := os.Open("test.go")     if err != nil {         log.Fatal(err)     }     // TODO: use fd. }
  要消除对未使用的导入的抱怨,请使用空白标识符来引用导入包中的符号。类似地,将未使用的变量分配给 fd  空白标识符将使未使用的变量错误消失。该版本的程序确实可以编译。package main  import (     "fmt"     "io"     "log"     "os" )  var _ = fmt.Printf // For debugging; delete when done. var _ io.Reader    // For debugging; delete when done.  func main() {     fd, err := os.Open("test.go")     if err != nil {         log.Fatal(err)     }     // TODO: use fd.     _ = fd }
  按照惯例,用于消除导入错误的全局声明应该在导入之后立即出现并进行注释,以便于找到它们并提醒以后进行清理。
  导入副作用
  最终应使用或删除 类似 fmt 或io 在前面的示例中未使用的导入:空白分配将代码标识为正在进行的工作。但有时导入一个包只是为了它的副作用是有用的,而没有任何明确的使用。例如,在其init 功能期间,net/http/pprof  包注册提供调试信息的 HTTP 处理程序。它有一个导出的 API,但大多数客户端只需要处理程序注册并通过网页访问数据。要仅为其副作用导入包,请将包重命名为空白标识符:import _ "net/http/pprof"
  这种导入形式清楚地表明该包是为了它的副作用而被导入的,因为该包没有其他可能的用途:在这个文件中,它没有名称。(如果是这样,而且我们没有使用该名称,编译器将拒绝该程序。)
  接口检查
  正如我们在上面对接口的讨论中看到的,一个类型不需要明确声明它实现了一个接口。相反,类型仅通过实现接口的方法来实现接口。实际上,大多数接口转换都是静态的,因此在编译时进行检查。例如,将 an 传递 *os.File 给期望 an 的函数io.Reader 将不会编译,除非 *os.File 实现该io.Reader 接口。
  但是,某些接口检查确实在运行时发生。一个实例在 encoding/json  包中,它定义了一个Marshaler  接口。当 JSON 编码器接收到实现该接口的值时,编码器调用该值的封送处理方法将其转换为 JSON,而不是执行标准转换。编码器在运行时使用如下类型断言检查此属性:m, ok := val.(json.Marshaler)
  如果只需要询问类型是否实现了接口,而不实际使用接口本身,也许作为错误检查的一部分,请使用空白标识符来忽略类型断言的值: if _, ok := val.(json.Marshaler); ok {     fmt.Printf("value %v of type %T implements json.Marshaler ", val, val) }
  出现这种情况的一个地方是当需要在实现类型的包中保证它实际满足接口时。如果一个类型——例如——  json.RawMessage 需要一个自定义的 JSON 表示,它应该实现 json.Marshaler ,但没有静态转换会导致编译器自动验证这一点。如果类型无意中未能满足接口,JSON 编码器仍然可以工作,但不会使用自定义实现。为了保证实现的正确性,可以在包中使用使用空白标识符的全局声明:var _ json.Marshaler = (*RawMessage)(nil)
  在这个声明中,涉及的转换分配  *RawMessage 到Marshaler  需要*RawMessage 工具Marshaler ,并且该属性将在编译时进行检查。如果json.Marshaler 接口发生变化,这个包将不再编译,我们会注意到它需要更新。
  此构造中出现的空白标识符表明该声明仅用于类型检查,而不是创建变量。但是,不要对满足接口的每种类型都这样做。按照惯例,只有在代码中不存在静态转换时才使用此类声明,这种情况很少见。 嵌入
  Go 没有提供典型的、类型驱动的子类化概念,但它确实有能力通过在结构或接口中 嵌入 类型来"借用"实现的片段。
  界面嵌入非常简单。我们之前提到过 io.Reader 和io.Writer 接口;这是他们的定义。type Reader interface {     Read(p []byte) (n int, err error) }  type Writer interface {     Write(p []byte) (n int, err error) }
  该 io 包还导出了几个其他接口,这些接口指定了可以实现多个此类方法的对象。例如,有io.ReadWriter 一个包含Read 和的接口Write 。我们可以io.ReadWriter 通过显式列出这两个方法来指定,但是嵌入这两个接口以形成新的接口更容易,更令人回味,如下所示:// ReadWriter is the interface that combines the Reader and Writer interfaces. type ReadWriter interface {     Reader     Writer }
  这就是它的样子:A ReadWriter 可以做 aReader 所做的和aWriter  所做的;它是嵌入式接口的联合。只有接口可以嵌入到接口中。
  同样的基本思想适用于结构,但具有更深远的影响。所述 bufio 封装具有两个结构类型, bufio.Reader 并且bufio.Writer ,其中每个过程器具从包的类似接口的 io 。并且bufio 还实现了一个缓冲的读取器/写入器,它通过使用嵌入将读取器和写入器组合到一个结构中来实现:它列出了结构中的类型但不给它们字段名称。// ReadWriter stores pointers to a Reader and a Writer. // It implements io.ReadWriter. type ReadWriter struct {     *Reader  // *bufio.Reader     *Writer  // *bufio.Writer }
  嵌入的元素是指向结构的指针,当然在使用之前必须初始化为指向有效的结构。该 ReadWriter 结构可以写成type ReadWriter struct {     reader *Reader     writer *Writer }
  但是为了提升字段的方法并满足 io 接口,我们还需要提供转发方法,如下所示:func (rw *ReadWriter) Read(p []byte) (n int, err error) {     return rw.reader.Read(p) }
  通过直接嵌入结构,我们避免了这种簿记。嵌入类型的方法是免费出现的,这意味着 bufio.ReadWriter  不仅有bufio.Reader and的方法,而且bufio.Writer 满足所有三个接口: io.Reader , io.Writer , 和 io.ReadWriter 。
  嵌入与子类化有一个重要的区别。当我们嵌入一个类型时,该类型的方法成为外部类型的方法,但是当它们被调用时,方法的接收者是内部类型,而不是外部类型。在我们的例子中,当a的 Read 方法bufio.ReadWriter 被调用时,和上面写的转发方法的效果完全一样;接收者是 的reader 字段,而ReadWriter 不是 ReadWriter 本身。
  嵌入也可以是一种简单的便利。此示例显示了一个嵌入的字段,旁边是一个常规的命名字段。 type Job struct {     Command string     *log.Logger }
  该 Job 类型现在有Print ,Printf ,Println  和其他方法*log.Logger 。Logger  当然,我们可以给a 字段名称,但没有必要这样做。现在,一旦初始化,我们就可以登录到Job :job.Println("starting now...")
  该 Logger 是有规律场Job 结构,所以我们可以用通常的方法进行初始化的构造函数中进行Job ,这样,func NewJob(command string, logger *log.Logger) *Job {     return &Job{command, logger} }
  或使用复合文字, job := &Job{command, log.New(os.Stderr, "Job: ", log.Ldate)}
  如果我们需要直接引用一个嵌入的字段,忽略包限定符的字段的类型名称作为字段名称,就像在 Read 我们的ReadWriter 结构体的方法中一样。在这里,如果我们需要访问 *log.Logger 一个的Job 变量job ,我们会写job.Logger ,如果我们想要改进的方法,这将是有益的Logger 。func (job *Job) Printf(format string, args ...interface{}) {     job.Logger.Printf("%q: %s", job.Command, fmt.Sprintf(format, args...)) }
  嵌入类型引入了名称冲突的问题,但解决它们的规则很简单。首先,字段或方法在类型的更深层嵌套部分 X 隐藏任何其他项目X 。如果log.Logger 包含名为 的字段或方法Command ,则 的Command 字段Job 将支配它。
  其次,如果同名出现在同一个嵌套层次,通常是错误的; log.Logger 如果Job 结构包含另一个名为 的字段或方法,则嵌入将是错误的Logger 。但是,如果在类型定义之外的程序中从未提到过重名,那也没关系。此限定提供了一些防止对从外部嵌入的类型进行更改的保护;如果添加的字段与另一个子类型中的另一个字段冲突,如果这两个字段都没有使用过,则没有问题。并发
  通过交流分享
  并发编程是一个很大的话题,这里只讨论一些 Go 特定的亮点。
  由于实现对共享变量的正确访问所需的微妙之处,许多环境中的并发编程变得困难。Go 鼓励一种不同的方法,在这种方法中,共享值在通道上传递,实际上,从不由单独的执行线程主动共享。在任何给定时间,只有一个 goroutine 可以访问该值。按照设计,不会发生数据竞争。为了鼓励这种思维方式,我们将其简化为一个口号:
  不要通过共享内存进行通信;相反,通过通信共享内存。
  这种方法可能太过分了。例如,最好通过在整数变量周围放置互斥锁来完成引用计数。但作为一种高级方法,使用通道来控制访问可以更轻松地编写清晰、正确的程序。
  考虑此模型的一种方法是考虑在一个 CPU 上运行的典型单线程程序。它不需要同步原语。现在运行另一个这样的实例;它也不需要同步。现在让这两个人交流;如果通信是同步器,则仍然不需要其他同步。例如,Unix 管道就非常适合这个模型。尽管 Go 的并发方法起源于 Hoare 的 Communicating Sequential Processes (CSP),但它也可以被视为 Unix 管道的类型安全泛化。
  协程
  它们被称为 goroutines 是因为现有的术语——线程、协程、进程等——传达了不准确的内涵。goroutine 有一个简单的模型:它是一个与同一地址空间中的其他 goroutine 并发执行的函数。它是轻量级的,成本比分配堆栈空间多一点。并且堆栈开始时很小,因此它们很便宜,并且可以通过根据需要分配(和释放)堆存储来增长。
  Goroutines 被多路复用到多个 OS 线程上,所以如果一个应该阻塞,例如在等待 I/O 时,其他人继续运行。他们的设计隐藏了线程创建和管理的许多复杂性。
  使用 go  关键字为函数或方法调用添加前缀以在新的 goroutine 中运行调用。当调用完成时,goroutine 静默退出。(效果类似于 Unix shell 的& 在后台运行命令的 符号。)go list.Sort() // 同时运行 list.Sort; 不要等它。
  函数文字在 goroutine 调用中很方便。 func Announce(message string, delay time.Duration) {     go func() {         time.Sleep(delay)         fmt.Println(message)     }()  // Note the parentheses - must call the function. }
  在 Go 中,函数文字是闭包:实现确保函数引用的变量只要它们处于活动状态就可以存活。
  这些示例不太实用,因为这些函数无法发出完成信号。为此,我们需要渠道。
  Channel
  与映射一样,通道分配有 make ,结果值充当对底层数据结构的引用。如果提供了一个可选的整数参数,它会设置通道的缓冲区大小。对于无缓冲或同步通道,默认值为零。ci := make(chan int) // 无缓冲的整数通道 cj := make(chan int, 0) // 无缓冲的整数通道 cs := make(chan *os.File, 100) // 指向文件的指针的缓冲通道
  无缓冲通道将通信(值的交换)与同步相结合,确保两个计算(goroutine)处于已知状态。
  有很多使用频道的好习语。这是让我们开始的一个。在上一节中,我们在后台启动了排序。通道可以允许启动 goroutine 等待排序完成。 c := make(chan int)  // Allocate a channel. // Start the sort in a goroutine; when it completes, signal on the channel. go func() {     list.Sort()     c <- 1  // Send a signal; value does not matter. }() doSomethingForAWhile() <-c   // Wait for sort to finish; discard sent value.
  接收器总是阻塞直到有数据要接收。如果通道未缓冲,则发送方会阻塞,直到接收方收到该值。如果通道有缓冲区,则发送方只会阻塞,直到值被复制到缓冲区;如果缓冲区已满,这意味着等待某个接收器检索到一个值。
  缓冲通道可以像信号量一样使用,例如限制吞吐量。在这个例子中,传入的请求被传递到 handle ,它向通道发送一个值,处理请求,然后从通道接收一个值,为下一个消费者准备"信号量"。通道缓冲区的容量将同时调用的数量限制为process 。var sem = make(chan int, MaxOutstanding)  func handle(r *Request) {     sem <- 1    // Wait for active queue to drain.     process(r)  // May take a long time.     <-sem       // Done; enable next request to run. }  func Serve(queue chan *Request) {     for {         req := <-queue         go handle(req)  // Don"t wait for handle to finish.     } }
  一旦 MaxOutstanding 处理程序正在执行process ,任何更多将阻止尝试发送到已填充的通道缓冲区,直到现有处理程序之一完成并从缓冲区接收。
  但是,这种设计有一个问题: Serve  为每个传入的请求创建一个新的 goroutine,即使只有MaxOutstanding  它们中的一个可以在任何时候运行。因此,如果请求来得太快,程序可能会消耗无限资源。我们可以通过改变Serve goroutine 的创建来解决这个缺陷。这是一个明显的解决方案,但请注意它有一个错误,我们将在随后修复:func Serve(queue chan *Request) {     for req := range queue {         sem <- 1         go func() {             process(req) // Buggy; see explanation below.             <-sem         }()     } }
  错误在于,在 Go for 循环中,每次迭代都会重用循环变量,因此该req  变量在所有 goroutine 之间共享。那不是我们想要的。我们需要确保req 每个 goroutine 都是独一无二的。这是一种方法,将 的值req 作为参数传递给 goroutine 中的闭包:func Serve(queue chan *Request) {     for req := range queue {         sem <- 1         go func(req *Request) {             process(req)             <-sem         }(req)     } }
  将此版本与前一个版本进行比较,以查看闭包声明和运行方式的不同之处。另一种解决方案是创建一个同名的新变量,如下例所示: func Serve(queue chan *Request) {     for req := range queue {         req := req // Create new instance of req for the goroutine.         sem <- 1         go func() {             process(req)             <-sem         }()     } }
  写起来可能有点奇怪 req := req
  但在 Go 中这样做是合法和惯用的。你会得到一个同名变量的新版本,故意在本地隐藏循环变量,但每个 goroutine 都是唯一的。
  回到编写服务器的一般问题,另一种很好地管理资源的方法是启动固定数量的 handle goroutines,全部从请求通道读取。goroutine 的数量限制了同时调用的数量process 。此Serve 函数还接受一个通道,在该通道上它将被告知退出;启动 goroutines 后,它会阻止从该通道接收。func handle(queue chan *Request) {     for r := range queue {         process(r)     } }  func Serve(clientRequests chan *Request, quit chan bool) {     // Start handlers     for i := 0; i < MaxOutstanding; i++ {         go handle(clientRequests)     }     <-quit  // Wait to be told to exit. }
  Channels of channels
  Go 最重要的特性之一是通道是一流的值,可以像任何...

用老A15芯片,iPhone14继续降价?那国产安卓机难受了去年iPhone13发布,采取了一波与国产安卓机截然相反的操作,在国产机们都在涨价的同时,苹果是加量的前提下,还逆市降价,然后iPhone13就卖爆了。2021年整个4季度,苹果在微信这个设置不关闭,陌生人就能随意看到你的隐私,记得告诉家人本文编辑今日头条作者维权骑士签约用户小俊技术分享独家原创制作未经授权严禁转载,发现抄袭者将进行全网维权投诉分享生活小妙招,享受科技新生活!大家好,欢迎来到今天的知识分享!我是你们的iPhone14全系建模图亮相,iPhone13命运悲惨一夜沦为白菜机根据供应链的一些消息,目前iPhone14系列的设计已经基本敲定,至少在外观方面,不会有太大的变化。目前很多配件厂商也通过各种方式提前获取了全系列的型号数据,用于提前准备配件产品,新华全媒丨我国累计建成开通5G基站155。9万个新华社北京4月19日电(记者张辛欣)记者从19日国新办举行的新闻发布会上获悉,一季度,我国5G基站新增13。4万个,累计建成开通155。9万个,5G网络已覆盖全国所有地级市和县城城俄罗斯制定半导体发展计划通过逆向工程抄近路,2030年量产28nm4月18日消息,根据外媒tomshardware的报导,在俄乌冲突爆发之后,而受到西方国家的联合制裁,俄罗斯已无法从相关供海外应商处取得所需的芯片,同时其国内的晶圆制造也受到了制裁中国邮政为什么还不倒闭?中国邮政为什么还不倒闭?中国邮政快递有一则新闻很好笑,有个人网购苹果,结果15天送到后苹果全烂了。这个买家发文邀请邮政各个配送环节的部门一起来吃烂苹果。喷子们很快对中国邮政发起群嘲python删除某一路径下的所有文件和文件夹importshutilimportosfilepathCUsersAdministratorDesktop新建文件夹defdelfile(filepath)删除某一路径下的所有文这一赛道,九号公司用中国智造引领全球电动滑板车赛道迎来了一座里程碑。4月13日,九号公司公布其智能电动滑板车累计量产突破1000万。里程碑的背后,是电动滑板车赛道在近些年实现了爆发式增长。数据显示,2017年,电动滑港股开盘恒生指数跌1。92,招商银行跌超10,美团跌5。61金融界4月19日消息今日香港恒生指数开盘跌1。92,报21106点,国企指数跌2。12,报7228。98点,红筹指数跌0。5,报4094。53点,恒生科技指数跌2。41,报4215蚂蚁集团战略投资2C2P加快Alipay在东南亚布局21世纪经济报道记者陶力郑植文上海报道4月18日,蚂蚁集团宣布和全球支付平台2C2P达成战略合作伙伴关系。据了解,作为东南亚支付行业的市场领导者之一,2C2P连接了泰国新加坡马来西谷歌公司否认了资料图新华社美联据俄罗斯塔斯社18日报道,谷歌公司一位发言人称,谷歌并没有为了披露俄罗斯军事和战略要地而更改其地图服务。报道称,谷歌公司的一位发言人在回应塔斯社置评请求时说,关于俄
国产电池巨头最终还是破产,负债197亿,没能从风口上走出来正如有人曾经说过,在机遇来临之际,站在风口处猪都能飞起来。似乎这句话很符合当前的不少的创业公司,有的公司一遇风云变化龙,但也有的公司在风口上被无情地淘汰,被时代给抛弃。在如今新能源蔚来营收高小鹏卖得多理想亏得少谁能率先盈利?来源中国新闻网中新财经3月30日电(葛成)近日,小鹏汽车2021财报出炉。至此,蔚来小鹏理想三家造车新势力的年报已经全部公布。根据三家造车新势力年度财报显示,2021年,蔚来营收最网友投诉知乎乱扣费多扣几十个月,钱都扣没了银行发短信才知道Tech星球3月30日消息,近日,多位网友投诉称,知乎App的盐选会员存在乱扣费自动续费的行为,通过投诉平台,发现存在大量的此类问题投诉,有媒体统计,单从2022年初至今就有过百例阿里腾讯要握手互联互通了?其实大家都知道啊,阿里和腾讯各自领域,在我国互联网圈具有很高的影响力,它们一直来处于竞争位置,前些年的财富榜,两位马老板都竞争上过富豪一哥位置每年的义乌互联网大会,包括互联网圈子饭英利汽车2021年净利润1。49亿元,新能源客户收入占比提升至103月29日盘后,英利汽车披露2021年年报。数据显示,2021年公司营业收入45。95亿元净利润1。49亿元,基本每股收益0。10元。拟向全体股东每10股派发现金红利0。1元(含税快手CEO程一笑平台全年助超2000万用户创收,超60来自中低线城市3月29日,快手科技发布2021年四季度及全年业绩。数据显示,快手四季度DAU(平均日活跃用户)达3。23亿,同比增长19。2,MAU(平均月活跃用户)同比增长21。5达5。78亿宁德时代麒麟电池引关注,多家公司在互动平台回应鞭牛士3月30日消息,宁德时代将于今年4月发布麒麟电池的消息引发市场关注,相关概念股今天早盘大涨。有投资者在互动平台向相关公司提问,江苏国泰称,公司生产的锂离子电池电解液适用于麒麟996改为715?国家对超时加班出手了,对996说不文小杰996真的是福报吗?3月29日,我国许多地方正在加强对加班问题的整改。自3月以来,北京山东安徽河南广西青海湖南湖北江西等9个省份,都在重点调查和整改加班问题。而互联网企业和制华为手机的哈利波特咒语!试试用咒语操控你的手机吧刚刚发现了一个有意思的东西,用华为手机的人可以试一下,这是华为和哈利波特的梦幻联动,对华为手机的小艺念咒语阿瓦达啃大瓜捂脸可以直接息屏,身为一个哈利波特迷,我玩了好久,下面是我挨个五分钟系列之Redis大规模数据存储简述互联网场景下面临的主要技术问题之一是高并发大数据量。为了提高性能保护数据库通常会在数据库之上加一层缓存。目前常用的缓存是Redis,那么Redis如何存储大规模数据?如何将数据库的iPhone14核心黑科技亮相,iPhone13现爱疯价创感人史苹果的iPhone产品将在年度秋季发布会上亮相。作为全球最受关注的手机产品之一,每年新iPhone推出前都会有好几个传闻。此前有消息称,苹果今年将重点关注新iPhone的成像系统,