Go语言教程之边写边学:标准库:strings(6)

了解标准库是一个非常好的学习语言的方法。接下里的一些章节我们将详细研究标准库中的类型,方法,结合实际应用案例进行有趣的编程。

字符串处理是比较基础的库,我们从简单的库开始入门。

本节我们将继续用实例代码研究以下函数的用法。


func Trim(s, cutset string) string
Trim返回字符串s的一个片段,其中删除了片段集中包含的所有前导和尾部Unicode代码点。
package main

import (
	"fmt"
	"strings"
)

func main() {
	fmt.Print(strings.Trim("¡¡¡Hello, Gophers!!!", "!¡")) // Hello, Gophers
}

func TrimFunc(s string, f func(rune) bool) string
TrimFunc返回字符串s的一个片段,其中删除了满足f(c)的所有前导和尾部Unicode代码点。
package main

import (
	"fmt"
	"strings"
	"unicode"
)

func main() {
	fmt.Print(strings.TrimFunc("¡¡¡Hello, Gophers!!!", func(r rune) bool {
		return !unicode.IsLetter(r) && !unicode.IsNumber(r)
	}))// Hello, Gophers
}

func TrimLeft(s, cutset string) string
仅删除前导部分的指定字符。
package main

import (
	"fmt"
	"strings"
)

func main() {
	fmt.Print(strings.TrimLeft("¡¡¡Hello, Gophers!!!", "!¡")) // Hello, Gophers!!!
}

func TrimLeftFunc(s string, f func(rune) bool) string
仅删除满足f(c)的所有前导部分的指定字符。
package main

import (
	"fmt"
	"strings"
	"unicode"
)

func main() {
	fmt.Print(strings.TrimLeftFunc("¡¡¡Hello, Gophers!!!", func(r rune) bool {
		return !unicode.IsLetter(r) && !unicode.IsNumber(r)
	})) // Hello, Gophers!!!
}

func TrimPrefix(s, prefix string) string
删除前导部分的字符串,它与left系列函数不同之处是,需要匹配整个prefix字符串。
package main

import (
    "fmt"
    "strings"
)

func main() {
    fmt.Println(strings.TrimPrefix("¡¡¡Hello, Gophers!!!", "¡")) // ¡¡Hello, Gophers!!!
    fmt.Println(strings.TrimPrefix("¡¡¡Hello, Gophers!!!", "¡¡¡Hello, ")) // Gophers!!!
    fmt.Println(strings.TrimPrefix("¡¡¡Hello, Gophers!!!", "¡¡¡Howdy, ")) // ¡¡¡Hello, Gophers!!!
}

func TrimRight(s, cutset string) string
TrimRight返回字符串s的一个片段,去掉片段集中包含的所有尾部Unicode代码点。
若要删除后缀,请改用TrimSuffix。
package main

import (
	"fmt"
	"strings"
)

func main() {
	fmt.Print(strings.TrimRight("¡¡¡Hello, Gophers!!!", "!¡")) // ¡¡¡Hello, Gophers
}

func TrimRightFunc(s string, f func(rune) bool) string
TrimRightFunc返回字符串s的一个片段,去掉所有满足f(c)的尾随Unicode代码点。
package main

import (
	"fmt"
	"strings"
	"unicode"
)

func main() {
	fmt.Print(strings.TrimRightFunc("¡¡¡Hello, Gophers!!!", func(r rune) bool {
		return !unicode.IsLetter(r) && !unicode.IsNumber(r)
	}))
}

func TrimSpace(s string) string
TrimSpace返回字符串s的一个片段,去掉所有前导和尾随空白字符。
package main

import (
	"fmt"
	"strings"
)

func main() {
	fmt.Println(strings.TrimSpace(" \t\n Hello, Gophers \n\t\r\n")) // Hello, Gophers
}

func TrimSuffix(s, suffix string) string
删除结尾部分的字符串,它与right系列函数不同之处是,需要匹配整个prefix字符串。
package main

import (
	"fmt"
	"strings"
)

func main() {
	var s = "¡¡¡Hello, Gophers!!!"
	s = strings.TrimSuffix(s, ", Gophers!!!")
	s = strings.TrimSuffix(s, ", Marmots!!!")
	fmt.Print(s) // ¡¡¡Hello
}
Go语言教程之边写边学:标准库:strings(5)

了解标准库是一个非常好的学习语言的方法。接下里的一些章节我们将详细研究标准库中的类型,方法,结合实际应用案例进行有趣的编程。

字符串处理是比较基础的库,我们从简单的库开始入门。

本节我们将继续用实例代码研究以下函数的用法。


func ToLower(s string) string
转换为小写
package main

import (
	"fmt"
	"strings"
)

func main() {
	fmt.Println(strings.ToLower("Gopher")) // gopher
}

func ToLowerSpecial(c unicode.SpecialCase, s string) string
ToLowerSpecial返回字符串s的副本,其中所有Unicode字母都使用c指定的大小写映射映射到其小写。
package main

import (
	"fmt"
	"strings"
	"unicode"
)

func main() {
	fmt.Println(strings.ToLowerSpecial(unicode.TurkishCase, "Önnek İş")) // önnek iş
}

func ToTitle(s string) string
ToTitle返回字符串s的副本,其中所有Unicode字母映射到其Unicode标题大小写。
package main

import (
	"fmt"
	"strings"
)

func main() {
	// Compare this example to the Title example.
	fmt.Println(strings.ToTitle("her royal highness")) // HER ROYAL HIGHNESS
	fmt.Println(strings.ToTitle("loud noises")) // LOUD NOISES
	fmt.Println(strings.ToTitle("хлеб")) // ХЛЕБ
}

func ToTitleSpecial(c unicode.SpecialCase, s string) string
package main

import (
	"fmt"
	"strings"
	"unicode"
)

func main() {
	fmt.Println(strings.ToTitleSpecial(unicode.TurkishCase, "dünyanın ilk borsa yapısı Aizonai kabul edilir")) 
        // DÜNYANIN İLK BORSA YAPISI AİZONAİ KABUL EDİLİR
}

func ToUpper(s string) string
package main

import (
	"fmt"
	"strings"
)

func main() {
	fmt.Println(strings.ToUpper("Gopher"))
}

func ToUpperSpecial(c unicode.SpecialCase, s string) string
package main

import (
	"fmt"
	"strings"
	"unicode"
)

func main() {
	fmt.Println(strings.ToUpperSpecial(unicode.TurkishCase, "örnek iş")) // ÖRNEK İŞ
}

func ToValidUTF8(s, replacement string) string
ToValidUTF8返回字符串s的副本,每次运行无效的UTF-8字节序列时都会被替换字符串替换,替换字符串可能为空。
package main

import (
	"fmt"
	"strings"
)

func main() {
	fmt.Printf("%s\n", strings.ToValidUTF8("abc", "\uFFFD")) // abc
	fmt.Printf("%s\n", strings.ToValidUTF8("a\xffb\xC0\xAFc\xff", "")) // abc
	fmt.Printf("%s\n", strings.ToValidUTF8("\xed\xa0\x80", "abc")) // abc
}
Go语言教程之边写边学:标准库:strings(4)

了解标准库是一个非常好的学习语言的方法。接下里的一些章节我们将详细研究标准库中的类型,方法,结合实际应用案例进行有趣的编程。

字符串处理是比较基础的库,我们从简单的库开始入门。

本节我们将继续用实例代码研究以下函数的用法。


func Join(elems []string, sep string) string
以sep为连接符号,拼接elems数组,最终输出字符串。
package main

import (
	"fmt"
	"strings"
)

func main() {
	s := []string{"foo", "bar", "baz"}
	fmt.Println(strings.Join(s, ", ")) // foo, bar, baz
}

func Map(mapping func(rune) rune, s string) string
Map返回字符串s的副本,其中所有字符都根据映射函数进行了修改。如果映射返回负值,则该字符将从字符串中删除,不进行替换。
import (
	"fmt"
	"strings"
)

func main() {
	rot13 := func(r rune) rune {
		switch {
		case r >= 'A' && r <= 'Z':
			return 'A' + (r-'A'+13)%26
		case r >= 'a' && r <= 'z':
			return 'a' + (r-'a'+13)%26
		}
		return r
	}
	fmt.Println(strings.Map(rot13, "'Twas brillig and the slithy gopher...")) // 'Gjnf oevyyvt naq gur fyvgul tbcure...
}

func Repeat(s string, count int) string
Repeat返回一个由字符串s的计数副本组成的新字符串。如果计数为负数或(len(s)*count)的结果溢出,程序会panic。
package main

import (
	"fmt"
	"strings"
)

func main() {
	fmt.Println("ba" + strings.Repeat("na", 2)) // banana
}

func Replace(s, old, new string, n int) string
Replace返回字符串s的副本,其中前n个不重叠的旧实例被新实例替换。如果old为空,它将在字符串的开头和每个UTF-8序列之后进行匹配,从而为k-rune字符串生成最多k+1个替换。如果n<0,则对替换的数量没有限制。
package main

import (
    "fmt"
    "strings"
)

func main() {
    fmt.Println(strings.Replace("oink oink oink", "", "###", 1))
    fmt.Println(strings.Replace("oink oink oink", "", "###", 2))
    fmt.Println(strings.Replace("oink oink oink", "", "###", -1))
    fmt.Println(strings.Replace("oink oink oink", "k", "ky", 2))
    fmt.Println(strings.Replace("oink oink oink", "oink", "moo", -1))
}

输出:
###oink oink oink
###o###ink oink oink
###o###i###n###k### ###o###i###n###k### ###o###i###n###k###
oinky oinky oink
moo moo moo

func ReplaceAll(s, old, new string) string
ReplaceAll返回字符串s的一个副本,其中所有不重叠的旧实例都被新实例替换。如果old为空,它将在字符串的开头和每个UTF-8序列之后进行匹配,从而为k-rune字符串生成最多k+1个替换。
package main

import (
	"fmt"
	"strings"
)

func main() {
	fmt.Println(strings.ReplaceAll("oink oink oink", "oink", "moo")) // moo moo moo
}

func Split(s, sep string) []string
将切片s拆分为由sep分隔的所有子字符串,并在这些分隔符之间返回子字符串的切片。
如果s不包含sep并且sep不为空,则Split返回一个长度为1的切片,该切片的唯一元素是s。
如果sep为空,则Split会在每个UTF-8序列之后进行拆分。如果s和sep都为空,Split将返回一个空切片。
它相当于计数为-1的SplitN。
package main

import (
    "fmt"
    "strings"
)

func main() {
    fmt.Printf("%q\n", strings.Split("a,b,c", ","))
    fmt.Printf("%q\n", strings.Split("a man a plan a canal panama", "a "))
    fmt.Printf("%q\n", strings.Split(" xyz ", ""))
    fmt.Printf("%q\n", strings.Split("", ""))
    fmt.Printf("%q\n", strings.Split("", "Bernardo O'Higgins"))
}

输出:

["a" "b" "c"]
["" "man " "plan " "canal panama"]
[" " "x" "y" "z" " "]
[]
[""]

func SplitAfter(s, sep string) []string
与split的区别是,返回的每个字符串包含sep字符。
package main

import (
	"fmt"
	"strings"
)

func main() {
	fmt.Printf("%q\n", strings.SplitAfter("a,b,c", ",")) // ["a," "b," "c"]
}


func SplitAfterN(s, sep string, n int) []string
计数决定要返回的子字符串数:
n>0:最多n个子串;最后一个子字符串将是未拆分的余数。
n=0:结果为零(零个子字符串)
n<0:所有子字符串
package main

import (
    "fmt"
    "strings"
)

func main() {
    fmt.Printf("%q\n", strings.SplitAfterN("a,b,c", ",", 2)) // ["a," "b,c"]
    fmt.Printf("%q\n", strings.SplitAfterN("a,b,c", ",", 0)) // []
    fmt.Printf("%q\n", strings.SplitAfterN("a,b,c", ",", 5)) // ["a," "b," "c"]
    fmt.Printf("%q\n", strings.SplitAfterN("a,b,c", ",", -1)) // ["a," "b," "c"]
}

func SplitN(s, sep string, n int) []string
计数决定要返回的子字符串数:
n>0:最多n个子串;最后一个子字符串将是未拆分的余数。
n=0:结果为零(零个子字符串)
n<0:所有子字符串
package main

import (
	"fmt"
	"strings"
)

func main() {
	fmt.Printf("%q\n", strings.SplitN("a,b,c", ",", 2)) // ["a" "b,c"]
	z := strings.SplitN("a,b,c", ",", 0)
	fmt.Printf("%q (nil = %v)\n", z, z == nil) // [] (nil = true)
}
Go语言教程之边写边学:标准库:strings(3)

了解标准库是一个非常好的学习语言的方法。接下里的一些章节我们将详细研究标准库中的类型,方法,结合实际应用案例进行有趣的编程。

字符串处理是比较基础的库,我们从简单的库开始入门。

本节我们将继续用实例代码研究以下函数的用法。


func Index(s, substr string) int
返回substr在s中首次出现的位置,不存在时返回-1
package main

import (
	"fmt"
	"strings"
)

func main() {
	fmt.Println(strings.Index("chicken", "ken")) // 4
	fmt.Println(strings.Index("chicken", "dmr")) // -1
}

func IndexAny(s, chars string) int
返回chars中任意一个首次出现在s的字母位置
package main

import (
	"fmt"
	"strings"
)

func main() {
	fmt.Println(strings.IndexAny("chicken", "aeiouy")) // 2
	fmt.Println(strings.IndexAny("crwth", "aeiouy")) // -1
}

func IndexByte(s string, c byte) int
返回c出现在s中首次出现的位置,不存在时-1
package main

import (
	"fmt"
	"strings"
)

func main() {
	fmt.Println(strings.IndexByte("golang", 'g')) // 0
	fmt.Println(strings.IndexByte("gophers", 'h')) // 3
	fmt.Println(strings.IndexByte("golang", 'x')) // -1
}

func IndexFunc(s string, f func(rune) bool) int
返回s中首次满足函数f的字母位置,不存在则-1
package main

import (
	"fmt"
	"strings"
	"unicode"
)

func main() {
	f := func(c rune) bool {
		return unicode.Is(unicode.Han, c)
	}
	fmt.Println(strings.IndexFunc("Hello, 世界", f)) // 7
	fmt.Println(strings.IndexFunc("Hello, world", f)) // -1
}

func IndexRune(s string, r rune) int
返回r在s中首次出现的位置,不存在则-1
package main

import (
	"fmt"
	"strings"
)

func main() {
	fmt.Println(strings.IndexRune("chicken", 'k')) // 4
	fmt.Println(strings.IndexRune("chicken", 'd')) // -1
}

func LastIndex(s, substr string) int
substr在s中最后一次出现的位置,不存在则-1
package main

import (
	"fmt"
	"strings"
)

func main() {
	fmt.Println(strings.Index("go gopher", "go")) // 0
	fmt.Println(strings.LastIndex("go gopher", "go")) // 3
	fmt.Println(strings.LastIndex("go gopher", "rodent")) // -1
}


func LastIndexAny(s, chars string) int
返回chars中任意一个最后出现在s的字母位置,不存在则-1
package main

import (
	"fmt"
	"strings"
)

func main() {
	fmt.Println(strings.LastIndexAny("go gopher", "go")) // 4
	fmt.Println(strings.LastIndexAny("go gopher", "rodent")) // 8
	fmt.Println(strings.LastIndexAny("go gopher", "fail")) // -1
}


func LastIndexByte(s string, c byte) int
返回c之后出现在s的字母位置,不存在则-1
package main

import (
	"fmt"
	"strings"
)

func main() {
	fmt.Println(strings.LastIndexByte("Hello, world", 'l')) // 10
	fmt.Println(strings.LastIndexByte("Hello, world", 'o')) // 8
	fmt.Println(strings.LastIndexByte("Hello, world", 'x')) // -1
}

func LastIndexFunc(s string, f func(rune) bool) int
返回s中最后满足函数f的字母位置
package main

import (
	"fmt"
	"strings"
	"unicode"
)

func main() {
	fmt.Println(strings.LastIndexFunc("go 123", unicode.IsNumber)) // 5
	fmt.Println(strings.LastIndexFunc("123 go", unicode.IsNumber)) // 2
	fmt.Println(strings.LastIndexFunc("go", unicode.IsNumber)) // -1
}
Go语言教程之边写边学:标准库:strings(2)

了解标准库是一个非常好的学习语言的方法。接下里的一些章节我们将详细研究标准库中的类型,方法,结合实际应用案例进行有趣的编程。

字符串处理是比较基础的库,我们从简单的库开始入门。

本节我们将继续用实例代码研究以下函数的用法。

func Cut(s, sep string) (before, after string, found bool)
func CutPrefix(s, prefix string) (after string, found bool)
func CutSuffix(s, suffix string) (before string, found bool)
func EqualFold(s, t string) bool
func Fields(s string) []string
func FieldsFunc(s string, f func(rune) bool) []string
func HasPrefix(s, prefix string) bool
func HasSuffix(s, suffix string) bool
package main

import (
    "fmt"
    "strings"
    "unicode"
)

func main() {
    m := "hello this is strings."

    // cut用sep参数分割原字符串,返回3个参数,切割后左边部分,切割后右边部分,是否存在sep,不存在就不切割
    show := func(s, sep string) {
        before, after, found := strings.Cut(s, sep)
        fmt.Printf("Cut(%q, %q) = %q, %q, %v\n", s, sep, before, after, found)
    }
    show(m, "this")
    show(m, "is")
    show(m, "wow")

    // cutprefix用于去掉前缀,如果原字符串以sep开头就去掉这一部分,返回剩余部分,以及是否存在布尔值
    show2 := func(s, sep string) {
        after, found := strings.CutPrefix(s, sep)
        fmt.Printf("CutPrefix(%q, %q) = %q, %v\n", s, sep, after, found)
    }

    show2(m, "hello")
    show2(m, "this")
    show2(m, "wow")

    // cutsuffix用于去掉后缀,如果原字符串以sep结尾就去掉这一部分,返回剩余部分,以及是否存在布尔值
    show3 := func(s, sep string) {
        after, found := strings.CutSuffix(s, sep)
        fmt.Printf("CutSuffix(%q, %q) = %q, %v\n", s, sep, after, found)
    }

    show3(m, "strings")
    show3(m, "strings.")
    show3(m, "wow")

    // equalFold是否在utf8编码下,不区分大小写相等
    fmt.Println("Go equalFold go?", strings.EqualFold("Go", "go"))

    // fields用空白字符分割字符串,包括空格,多个空格,\t,\r,\n
    fmt.Println("Fields are 1: ", strings.Fields("  foo bar  baz   "))
    fmt.Println("Fields are 2: ", strings.Fields("  foo bar\t baz   "))
    fmt.Println("Fields are 3: ", strings.Fields("  foo bar\r\nbaz   "))

    // fieldsFunc根据提供的函数分割字符串,下面的f函数判断当c不是字母也不是数字的时候返回true,也就是在此处分割
    f := func(c rune) bool {
        return !unicode.IsLetter(c) && !unicode.IsNumber(c)
    }
    fmt.Println("Fields are 4: ", strings.FieldsFunc("  foo1;bar2,baz3...", f))

    // hasPrefix, 区分大小写
    fmt.Println(strings.HasPrefix(m, "Hello"))

    // hasSuffix, 区分大小写
    fmt.Println(strings.HasSuffix(m, "strings."))
}

输出:

Cut("hello this is strings.", "this") = "hello ", " is strings.", true
Cut("hello this is strings.", "is") = "hello th", " is strings.", true
Cut("hello this is strings.", "wow") = "hello this is strings.", "", false
CutPrefix("hello this is strings.", "hello") = " this is strings.", true
CutPrefix("hello this is strings.", "this") = "hello this is strings.", false
CutPrefix("hello this is strings.", "wow") = "hello this is strings.", false
CutSuffix("hello this is strings.", "strings") = "hello this is strings.", false
CutSuffix("hello this is strings.", "strings.") = "hello this is ", true
CutSuffix("hello this is strings.", "wow") = "hello this is strings.", false
Go equalFold go? true
Fields are 1:  [foo bar baz]
Fields are 2:  [foo bar baz]
Fields are 3:  [foo bar baz]
Fields are 4:  [foo1 bar2 baz3]
false
false
Go语言教程之边写边学:标准库:strings(1)

了解标准库是一个非常好的学习语言的方法。接下里的一些列章节我们将详细研究标准库中的类型,方法,结合实际应用案例进行有趣的编程。

字符串处理是比较基础的库,我们从简单的库开始入门。

先看一下strings相关的类型和方法,大概分为通用方法部分和Builder,Reader,Replacer类型。

func Clone(s string) string
func Compare(a, b string) int
func Contains(s, substr string) bool
func ContainsAny(s, chars string) bool
func ContainsFunc(s string, f func(rune) bool) bool
func ContainsRune(s string, r rune) bool
func Count(s, substr string) int
func Cut(s, sep string) (before, after string, found bool)
func CutPrefix(s, prefix string) (after string, found bool)
func CutSuffix(s, suffix string) (before string, found bool)
func EqualFold(s, t string) bool
func Fields(s string) []string
func FieldsFunc(s string, f func(rune) bool) []string
func HasPrefix(s, prefix string) bool
func HasSuffix(s, suffix string) bool
func Index(s, substr string) int
func IndexAny(s, chars string) int
func IndexByte(s string, c byte) int
func IndexFunc(s string, f func(rune) bool) int
func IndexRune(s string, r rune) int
func Join(elems []string, sep string) string
func LastIndex(s, substr string) int
func LastIndexAny(s, chars string) int
func LastIndexByte(s string, c byte) int
func LastIndexFunc(s string, f func(rune) bool) int
func Map(mapping func(rune) rune, s string) string
func Repeat(s string, count int) string
func Replace(s, old, new string, n int) string
func ReplaceAll(s, old, new string) string
func Split(s, sep string) []string
func SplitAfter(s, sep string) []string
func SplitAfterN(s, sep string, n int) []string
func SplitN(s, sep string, n int) []string
func Title(s string) stringDEPRECATED
func ToLower(s string) string
func ToLowerSpecial(c unicode.SpecialCase, s string) string
func ToTitle(s string) string
func ToTitleSpecial(c unicode.SpecialCase, s string) string
func ToUpper(s string) string
func ToUpperSpecial(c unicode.SpecialCase, s string) string
func ToValidUTF8(s, replacement string) string
func Trim(s, cutset string) string
func TrimFunc(s string, f func(rune) bool) string
func TrimLeft(s, cutset string) string
func TrimLeftFunc(s string, f func(rune) bool) string
func TrimPrefix(s, prefix string) string
func TrimRight(s, cutset string) string
func TrimRightFunc(s string, f func(rune) bool) string
func TrimSpace(s string) string
func TrimSuffix(s, suffix string) string
type Builder
func (b *Builder) Cap() int
func (b *Builder) Grow(n int)
func (b *Builder) Len() int
func (b *Builder) Reset()
func (b *Builder) String() string
func (b *Builder) Write(p []byte) (int, error)
func (b *Builder) WriteByte(c byte) error
func (b *Builder) WriteRune(r rune) (int, error)
func (b *Builder) WriteString(s string) (int, error)
type Reader
func NewReader(s string) *Reader
func (r *Reader) Len() int
func (r *Reader) Read(b []byte) (n int, err error)
func (r *Reader) ReadAt(b []byte, off int64) (n int, err error)
func (r *Reader) ReadByte() (byte, error)
func (r *Reader) ReadRune() (ch rune, size int, err error)
func (r *Reader) Reset(s string)
func (r *Reader) Seek(offset int64, whence int) (int64, error)
func (r *Reader) Size() int64
func (r *Reader) UnreadByte() error
func (r *Reader) UnreadRune() error
func (r *Reader) WriteTo(w io.Writer) (n int64, err error)
type Replacer
func NewReplacer(oldnew ...string) *Replacer
func (r *Replacer) Replace(s string) string
func (r *Replacer) WriteString(w io.Writer, s string) (n int, err error)

本节我们将用一些实例代码研究以下7个函数的用法。
func Clone(s string) string
func Compare(a, b string) int
func Contains(s, substr string) bool
func ContainsAny(s, chars string) bool
func ContainsFunc(s string, f func(rune) bool) bool
func ContainsRune(s string, r rune) bool
func Count(s, substr string) int

package main

import (
    "fmt"
    "strings"
    "unsafe"
)

func main() {
    m := "hello this is strings."
    c := strings.Clone(m)
    // 判断相同指针,clone会创建新副本,因此这里返回false
    fmt.Println(unsafe.StringData(c) == unsafe.StringData(m))
    // 比较,返回0则相等
    fmt.Println(strings.Compare(c, m))
    // 是否包含hello字符串
    fmt.Println(strings.Contains(c, "hello"))
    // 是否含有xyzh中的任意一个字母
    fmt.Println(strings.ContainsAny(c, "xyzh"))
    // 是否有z字母
    fmt.Println(strings.ContainsFunc(c, func(a rune) bool {
        return a == 'z'
    }))
    // 是否有a字母
    fmt.Println(strings.ContainsRune(c, 97))
    // 字母i出现几次
    fmt.Println(strings.Count(c, "i"))
}

输出:
false
0
true
true
false
false
3
Go语言教程之边写边学:了解如何在 Go 中处理错误
编写程序时,需要考虑程序失败的各种方式,并且需要管理失败。 无需让用户看到冗长而混乱的堆栈跟踪错误。 让他们看到有关错误的有意义的信息更好。 正如你所看到的,Go具有panic和recover之类的内置函数来管理程序中的异常或意外行为。 但错误是已知的失败,你的程序应该可以处理它们。
Go的错误处理方法只是一种只需要if和return语句的控制流机制。 例如,在调用函数以从employee对象获取信息时,可能需要了解该员工是否存在。 Go处理此类预期错误的一贯方法如下所示:
employee, err := getInformation(1000)
if err != nil {
    // Something is wrong. Do something.
}
注意getInformation函数返回了employee结构,还返回了错误作为第二个值。 该错误可能为nil。 如果错误为nil,则表示成功。 如果错误不是nil,则表示失败。 非nil错误附带一条错误消息,你可以打印该错误消息,也可以记录该消息(更可取)。 这是在Go中处理错误的方式。 下一部分将介绍一些其他策略。
你可能会注意到,Go中的错误处理要求你更加关注如何报告和处理错误。 这正是问题的关键。 让我们看一些其他示例,以帮助你更好地了解Go的错误处理方法。
我们将使用用于结构的代码片段来练习各种错误处理策略:
package main

import (
    "fmt"
    "os"
)

type Employee struct {
    ID        int
    FirstName string
    LastName  string
    Address   string
}

func main() {
    employee, err := getInformation(1001)
    if err != nil {
        // Something is wrong. Do something.
    } else {
        fmt.Print(employee)
    }
}

func getInformation(id int) (*Employee, error) {
    employee, err := apiCallEmployee(1000)
    return employee, err
}

func apiCallEmployee(id int) (*Employee, error) {
    employee := Employee{LastName: "Doe", FirstName: "John"}
    return &employee, nil
}
从现在开始,我们将重点介绍如何修改getInformation、apiCallEmployee和main函数,以展示如何处理错误。

错误处理策略

当函数返回错误时,该错误通常是最后一个返回值。 正如上一部分所介绍的那样,调用方负责检查是否存在错误并处理错误。 因此,一个常见策略是继续使用该模式在子例程中传播错误。 例如,子例程(如上一示例中的getInformation)可能会将错误返回给调用方,而不执行其他任何操作,如下所示:
func getInformation(id int) (*Employee, error) {
    employee, err := apiCallEmployee(1000)
    if err != nil {
        return nil, err // Simply return the error to the caller.
    }
    return employee, nil
}

你可能还需要在传播错误之前添加更多信息。 为此,可以使用fmt.Errorf() 函数,该函数与我们之前看到的函数类似,但它返回一个错误。 例如,你可以向错误添加更多上下文,但仍返回原始错误,如下所示:

func getInformation(id int) (*Employee, error) {
    employee, err := apiCallEmployee(1000)
    if err != nil {
        return nil, fmt.Errorf("Got an error when getting the employee information: %v", err)
    }
    return employee, nil
}

另一种策略是在错误为暂时性错误时运行重试逻辑。 例如,可以使用重试策略调用函数三次并等待两秒钟,如下所示:

func getInformation(id int) (*Employee, error) {
    for tries := 0; tries < 3; tries++ {
        employee, err := apiCallEmployee(1000)
        if err == nil {
            return employee, nil
        }

        fmt.Println("Server is not responding, retrying ...")
        time.Sleep(time.Second * 2)
    }

    return nil, fmt.Errorf("server has failed to respond to get the employee information")
}
最后,可以记录错误并对最终用户隐藏任何实现详细信息,而不是将错误打印到控制台。 我们将在下一模块介绍日志记录。 现在,让我们看看如何创建和使用自定义错误。

创建可重用的错误

有时错误消息数会增加,你需要维持秩序。 或者,你可能需要为要重用的常见错误消息创建一个库。 在Go中,你可以使用errors.New() 函数创建错误并在若干部分中重复使用这些错误,如下所示:
var ErrNotFound = errors.New("Employee not found!")

func getInformation(id int) (*Employee, error) {
    if id != 1001 {
        return nil, ErrNotFound
    }

    employee := Employee{LastName: "Doe", FirstName: "John"}
    return &employee, nil
}
getInformation函数的代码外观更优美,而且如果需要更改错误消息,只需在一个位置更改即可。 另请注意,惯例是为错误变量添加Err前缀。
最后,如果你具有错误变量,则在处理调用方函数中的错误时可以更具体。 errors.Is() 函数允许你比较获得的错误的类型,如下所示:
employee, err := getInformation(1000)
if errors.Is(err, ErrNotFound) {
    fmt.Printf("NOT FOUND: %v\n", err)
} else {
    fmt.Print(employee)
}

用于错误处理的推荐做法

在Go中处理错误时,请记住下面一些推荐做法:
  • 始终检查是否存在错误,即使预期不存在。 然后正确处理它们,以免向最终用户公开不必要的信息。
  • 在错误消息中包含一个前缀,以便了解错误的来源。 例如,可以包含包和函数的名称。
  • 创建尽可能多的可重用错误变量。
  • 了解使用返回错误和panic之间的差异。 不能执行其他操作时再使用panic。 例如,如果某个依赖项未准备就绪,则程序运行无意义(除非你想要运行默认行为)。
  • 在记录错误时记录尽可能多的详细信息(我们将在下一部分介绍记录方法),并打印出最终用户能够理解的错误。
Go语言教程之边写边学:编写并测试程序 :完成银行项目功能
你的程序已具备一些基本功能。 但是,它还缺少一项功能:向其他帐户转账的功能。 这一挑战包括添加该功能,以及我们认为可为现有API增加价值的另一项功能。

实现转账方法

若要创建转账方法,应牢记以下几点:

  • 你需要实现向其他帐户转账的功能。 在本例中,你必须使用至少两个帐户来初始化程序,而不是像之前一样只使用一个帐户。
  • 由于你要在核心程序包中添加新方法,因此请首先创建测试用例,以确保你编写正确的逻辑来进行转账。 请密切注意在函数与指针之间进行通信的方式。
  • 你的转账方法应当接收你要转账的金额以及你将在其中增加资金的帐户对象。 请确保重用存款和取款方法以避免重复(特别是对于错误处理)。
  • 请记住,如果你没有足够的资金,则无法向其他帐户转账。

修改对账单终结点以返回JSON对象

当前,/statement终结点会返回一个字符串。如果你想将其公开为API,则该字符串没有用处。 修改终结点以采用JSON格式返回帐户对象:
"{\"Name\":\"John\",\"Address\":\"Los Angeles, California\",\"Phone\":\"(213) 555 0147\",\"Number\":1001,\"Balance\":0}"
我们希望你假定使用你的核心程序包的用户有可能希望实施不同的对账单方法来更改输出。 因此,你需要进行适当的更改,以使核心程序包具有可扩展性。 换句话说,你需要执行以下操作:
创建一个包含Statement() string函数的接口。
在核心程序包中创建一个新的Statement() 函数,该函数接收你以参数形式创建的接口。 此函数应该调用你的结构已有的Statement() 方法。
当你进行这些更改时,系统会允许你创建自定义Account结构和自定义Statement() 方法。 如果不记得如何执行这些编码更改,你可以返回到有关结构(嵌入)和接口的模块。

下面是解决此模块中的两项挑战的完整代码。
银行核心测试:bank_test.go
//...

func TestTransfer(t *testing.T) {
    accountA := Account{
		Customer: Customer{
			Name:    "John",
			Address: "Los Angeles, California",
			Phone:   "(213) 555 0147",
		},
		Number:  1001,
		Balance: 0,
	}

	accountB := Account{
		Customer: Customer{
			Name:    "Mark",
			Address: "Irvine, California",
			Phone:   "(949) 555 0198",
		},
		Number:  1002,
		Balance: 0,
	}

	accountA.Deposit(100)
	err := accountA.Transfer(50, &accountB)

	if accountA.Balance != 50 && accountB.Balance != 50 {
		t.Error("transfer from account A to account B is not working", err)
	}
}

银行核心:bank.go

package bank

import (
	"errors"
	"fmt"
)

//...

// Transfer function
func (a *Account) Transfer(amount float64, dest *Account) error {
	if amount <= 0 {
		return errors.New("the amount to transfer should be greater than zero")
	}

	if a.Balance < amount {
		return errors.New("the amount to transfer should be greater than the account's balance")
	}

	a.Withdraw(amount)
	dest.Deposit(amount)
	return nil
}

// Bank ...
type Bank interface {
	Statement() string
}

// Statement ...
func Statement(b Bank) string {
	return b.Statement()
}

银行API:main.go

package main

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"strconv"

	"github.com/msft/bank"
)

var accounts = map[float64]*CustomAccount{}

func main() {
	accounts[1001] = &CustomAccount{
		Account: &bank.Account{
			Customer: bank.Customer{
				Name:    "John",
				Address: "Los Angeles, California",
				Phone:   "(213) 555 0147",
			},
			Number: 1001,
		},
	}

	accounts[1002] = &CustomAccount{
		Account: &bank.Account{
			Customer: bank.Customer{
				Name:    "Mark",
				Address: "Irvine, California",
				Phone:   "(949) 555 0198",
			},
			Number: 1002,
		},
	}

	http.HandleFunc("/statement", statement)
	http.HandleFunc("/deposit", deposit)
	http.HandleFunc("/withdraw", withdraw)
	http.HandleFunc("/transfer", transfer)
	log.Fatal(http.ListenAndServe("localhost:8000", nil))
}

//...

func transfer(w http.ResponseWriter, req *http.Request) {
	numberqs := req.URL.Query().Get("number")
	amountqs := req.URL.Query().Get("amount")
	destqs := req.URL.Query().Get("dest")

	if numberqs == "" {
		fmt.Fprintf(w, "Account number is missing!")
		return
	}

	if number, err := strconv.ParseFloat(numberqs, 64); err != nil {
		fmt.Fprintf(w, "Invalid account number!")
	} else if amount, err := strconv.ParseFloat(amountqs, 64); err != nil {
		fmt.Fprintf(w, "Invalid amount number!")
	} else if dest, err := strconv.ParseFloat(destqs, 64); err != nil {
		fmt.Fprintf(w, "Invalid account destination number!")
	} else {
		if accountA, ok := accounts[number]; !ok {
			fmt.Fprintf(w, "Account with number %v can't be found!", number)
		} else if accountB, ok := accounts[dest]; !ok {
			fmt.Fprintf(w, "Account with number %v can't be found!", dest)
		} else {
			err := accountA.Transfer(amount, accountB.Account)
			if err != nil {
				fmt.Fprintf(w, "%v", err)
			} else {
				fmt.Fprintf(w, accountA.Statement())
			}
		}
	}
}

func statement(w http.ResponseWriter, req *http.Request) {
	numberqs := req.URL.Query().Get("number")

	if numberqs == "" {
		fmt.Fprintf(w, "Account number is missing!")
		return
	}

	number, err := strconv.ParseFloat(numberqs, 64)
	if err != nil {
		fmt.Fprintf(w, "Invalid account number!")
	} else {
		account, ok := accounts[number]
		if !ok {
			fmt.Fprintf(w, "Account with number %v can't be found!", number)
		} else {
			json.NewEncoder(w).Encode(bank.Statement(account))
		}
	}
}

// CustomAccount ...
type CustomAccount struct {
	*bank.Account
}

// Statement ...
func (c *CustomAccount) Statement() string {
	json, err := json.Marshal(c)
	if err != nil {
		return err.Error()
	}

	return string(json)
}
Go语言教程之边写边学:编写并测试程序 :编写银行 API
现在,我们已经构建了网上银行的核心逻辑,接下来让我们构建一个Web API,以通过浏览器(甚至是命令行)对该逻辑进行测试。 目前,我们不使用数据库来持久保存数据,因此必须创建一个全局变量,以便将所有帐户存储在内存中。
此外,我们将跳过测试部分,以免本指南的操作持续太长时间。 理想情况下,你应该遵循我们在构建核心程序包时遵循的相同方法,在编写代码之前编写测试。

在内存中设置帐户

我们将为帐户使用在程序启动时创建的内存映射,而不是使用数据库来持久保存数据。 另外,我们将使用映射通过帐号来访问帐户信息。
转到 $GOPATH/src/bankapi/main.go文件,添加以下代码来创建全局accounts变量并使用一个帐户来初始化该变量。 (此代码类似于我们之前创建测试时添加的代码。)
package main

import (
    "github.com/msft/bank"
)

var accounts = map[float64]*bank.Account{}

func main() {
    accounts[1001] = &bank.Account{
        Customer: bank.Customer{
            Name:    "John",
            Address: "Los Angeles, California",
            Phone:   "(213) 555 0147",
        },
        Number: 1001,
    }
}
确保位于 $GOPATH/src/bankapi/ 位置。 使用go run main.go来运行程序以确保没有任何错误。 此程序目前不做任何其他事情,因此我们将添加逻辑来创建一个Web API。

公开对账单方法

正如你在以前的模块中看到的那样,采用Go创建Web API非常简单。 我们将继续使用net/http程序包。 我们还将使用HandleFunc和ListenAndServe函数来公开终结点并启动服务器。 HandleFunc函数需要一个你要公开的URL路径的名称,以及包含该终结点的逻辑的函数的名称。
首先,我们将公开用来输出某个帐户的对账单的功能。 将以下函数复制并粘贴到main.go中:
func statement(w http.ResponseWriter, req *http.Request) {
    numberqs := req.URL.Query().Get("number")

    if numberqs == "" {
        fmt.Fprintf(w, "Account number is missing!")
        return
    }

    if number, err := strconv.ParseFloat(numberqs, 64); err != nil {
        fmt.Fprintf(w, "Invalid account number!")
    } else {
        account, ok := accounts[number]
        if !ok {
            fmt.Fprintf(w, "Account with number %v can't be found!", number)
        } else {
            fmt.Fprintf(w, account.Statement())
        }
    }
}
statement函数的第一个重点是接收用于将响应写回到浏览器的对象 (w http.ResponseWriter)。 它还会接收用于访问HTTP请求中的信息的请求对象 (req *http.Request)。
然后请注意,我们使用req.URL.Query().Get() 函数从查询字符串读取参数。 此参数是我们将通过HTTP调用发送的帐号。 我们将使用该值来访问帐户映射并获取其信息。
由于我们要从用户那里获取数据,因此应包括一些验证以避免出现故障。 当我们知道自己具有有效帐号后,就可以调用Statement() 方法,并将它返回的字符串输出到浏览器 (fmt.Fprintf(w, account.Statement()))。
现在,修改main() 函数,使其类似于:
func main() {
    accounts[1001] = &bank.Account{
        Customer: bank.Customer{
            Name:    "John",
            Address: "Los Angeles, California",
            Phone:   "(213) 555 0147",
        },
        Number: 1001,
    }

    http.HandleFunc("/statement", statement)
    log.Fatal(http.ListenAndServe("localhost:8000", nil))
}

如果在运行程序 (go run main.go) 时未看到任何错误或输出,则表明它在正常运行。 打开Web浏览器并输入URL http://localhost:8000/statement?number=1001,或在程序运行时在另一个shell中运行以下命令:

curl http://localhost:8000/statement?number=1001

应会看到以下输出:

1001 - John - 0

公开存款方法

接下来,我们将继续使用相同的方法来公开存款方法。 在本例中,我们要向内存中的帐户增加资金。 每次调用Deposit() 方法时,余额都应当增加。
在主程序中,添加一个deposit() 函数,如下所示。 此函数从查询字符串中获取帐号,验证accounts映射中是否存在该帐户,验证要存入的金额是否为有效的数字,然后调用Deposit() 方法。
func deposit(w http.ResponseWriter, req *http.Request) {
    numberqs := req.URL.Query().Get("number")
    amountqs := req.URL.Query().Get("amount")

    if numberqs == "" {
        fmt.Fprintf(w, "Account number is missing!")
        return
    }

    if number, err := strconv.ParseFloat(numberqs, 64); err != nil {
        fmt.Fprintf(w, "Invalid account number!")
    } else if amount, err := strconv.ParseFloat(amountqs, 64); err != nil {
        fmt.Fprintf(w, "Invalid amount number!")
    } else {
        account, ok := accounts[number]
        if !ok {
            fmt.Fprintf(w, "Account with number %v can't be found!", number)
        } else {
            err := account.Deposit(amount)
            if err != nil {
                fmt.Fprintf(w, "%v", err)
            } else {
                fmt.Fprintf(w, account.Statement())
            }
        }
    }
}
请注意,此函数遵循类似的方法来获取和验证它从用户那里收到的数据。 我们还直接在if语句中声明和使用变量。 最后,将一些资金添加到帐户后,我们将输出对账单来查看新的帐户余额。
现在,你应公开一个调用deposit函数的 /deposit终结点。 将main() 函数修改为如下所示的形式:
    }

    http.HandleFunc("/statement", statement)
    http.HandleFunc("/deposit", deposit)
    log.Fatal(http.ListenAndServe("localhost:8000", nil))
}

如果在运行程序 (go run main.go) 时未看到任何错误或输出,则表明它在正常运行。 打开Web浏览器并输入URL http://localhost:8000/deposit?number=1001&amount=100,或在程序运行时在另一个shell中运行以下命令:

curl "http://localhost:8000/deposit?number=1001&amount=100"

应会看到以下输出:

1001 - John - 100
如果多次进行相同的调用,则帐户余额将继续增加。 尝试确认内存中的accounts映射在运行时是否更新。 如果你停止程序,则你存入的任何存款都会丢失,但在此初始版本中这是意料之中的情况。

公开取款方法

最后,让我们公开从帐户中取款的方法。 同样,我们将先在主程序中创建withdraw函数。 此函数将验证帐号信息,取款并输出从核心程序包收到的任何错误。 在主程序中添加以下函数:
func withdraw(w http.ResponseWriter, req *http.Request) {
    numberqs := req.URL.Query().Get("number")
    amountqs := req.URL.Query().Get("amount")

    if numberqs == "" {
        fmt.Fprintf(w, "Account number is missing!")
        return
    }

    if number, err := strconv.ParseFloat(numberqs, 64); err != nil {
        fmt.Fprintf(w, "Invalid account number!")
    } else if amount, err := strconv.ParseFloat(amountqs, 64); err != nil {
        fmt.Fprintf(w, "Invalid amount number!")
    } else {
        account, ok := accounts[number]
        if !ok {
            fmt.Fprintf(w, "Account with number %v can't be found!", number)
        } else {
            err := account.Withdraw(amount)
            if err != nil {
                fmt.Fprintf(w, "%v", err)
            } else {
                fmt.Fprintf(w, account.Statement())
            }
        }
    }
}

现在,在main() 函数中添加 /withdraw终结点以公开你在withdraw() 函数中实现的逻辑。 将main() 函数修改为如下所示的形式:

func main() {
    accounts[1001] = &bank.Account{
        Customer: bank.Customer{
            Name:    "John",
            Address: "Los Angeles, California",
            Phone:   "(213) 555 0147",
        },
        Number: 1001,
    }

    http.HandleFunc("/statement", statement)
    http.HandleFunc("/deposit", deposit)
    http.HandleFunc("/withdraw", withdraw)
    log.Fatal(http.ListenAndServe("localhost:8000", nil))
}

如果在运行程序 (go run main.go) 时未看到任何错误或输出,则表明它在正常运行。 打开Web浏览器并输入URL http://localhost:8000/withdraw?number=1001&amount=100,或在程序运行时在另一个shell中运行以下命令:

curl "http://localhost:8000/withdraw?number=1001&amount=100"

应会看到以下输出:

the amount to withdraw should be greater than the account's balance

请注意,我们收到的错误来自核心程序包。 当程序启动时,帐户余额为零。 因此,你无法提取任何金额的存款。 多次调用 /deposit终结点来添加资金,并再次调用 /withdraw终结点来确认它正常运行:

curl "http://localhost:8000/deposit?number=1001&amount=100"
curl "http://localhost:8000/deposit?number=1001&amount=100"
curl "http://localhost:8000/deposit?number=1001&amount=100"
curl "http://localhost:8000/withdraw?number=1001&amount=100"

应会看到以下输出:

1001 - John - 200

就这么简单! 你已创建了一个Web API,用于公开你从头构建的一个程序包中的功能。 请转到下一部分继续进行练习。 这次,你将编写自己的解决方案来完成一项挑战。

Go语言教程之边写边学:编写并测试程序 :编写银行核心程序包
现在,我们已经有了与我们的测试文件一起运行的基础项目,接下来我们将开始编写代码来实现上一个单元的功能和需求。 我们将回到我们讨论过的一些主题,例如错误、结构和方法。
打开 $GOPATH/src/bankcore/bank.go文件,删除Hello() 函数,然后开始编写网上银行系统的核心逻辑。

为客户和帐户创建结构

让我们先创建一个Customer结构,其中将包含要成为银行客户的人员的姓名、地址和电话号码。 此外,我们还需要Account数据的结构。 由于一个客户可以有多个帐户,因此让我们将客户信息嵌入到帐户对象中。 基本上,我们将创建已在TestAccount测试中定义的内容。
我们需要的结构可能类似于以下代码示例:
package bank

// Customer ...
type Customer struct {
    Name    string
    Address string
    Phone   string
}

// Account ...
type Account struct {
    Customer
    Number  int32
    Balance float64
}

现在,在终端中运行go test -v命令时,你应该会看到测试通过:

=== RUN   TestAccount
--- PASS: TestAccount (0.00s)
PASS
ok      github.com/msft/bank    0.094s
由于我们已实现了Customer和Account的结构,因此此测试通过。 现在我们已经有了这些结构,接下来让我们编写一些方法,用于在银行的初始版本中添加所需的功能。 这些功能包括存款、取款和转账。

实现存款方法

我们需要从一种允许将资金添加到帐户的方法开始。 但在执行此操作之前,让我们在bank_test.go文件中创建TestDeposit函数:
func TestDeposit(t *testing.T) {
    account := Account{
        Customer: Customer{
            Name:    "John",
            Address: "Los Angeles, California",
            Phone:   "(213) 555 0147",
        },
        Number:  1001,
        Balance: 0,
    }

    account.Deposit(10)

    if account.Balance != 10 {
        t.Error("balance is not being updated after a deposit")
    }
}

运行go test -v时,应该会在输出中看到一个将失败的测试:

# github.com/msft/bank [github.com/msft/bank.test]
./bank_test.go:32:9: account.Deposit undefined (type Account has no field or method Deposit)
FAIL    github.com/msft/bank [build failed]
为了满足前面的测试,让我们创建Account结构的Deposit方法。如果收到的金额等于或小于零,该方法会返回一个错误。 否则,只需将收到的金额添加到帐户的余额。
将以下代码用于Deposit方法:
// Deposit ...
func (a *Account) Deposit(amount float64) error {
    if amount <= 0 {
        return errors.New("the amount to deposit should be greater than zero")
    }

    a.Balance += amount
    return nil
}

运行go test -v时,应该会看到测试通过:

=== RUN   TestAccount
--- PASS: TestAccount (0.00s)
=== RUN   TestDeposit
--- PASS: TestDeposit (0.00s)
PASS
ok      github.com/msft/bank    0.193s

你还可以编写一个测试,

确认当尝试存入的金额为负时会出现错误,如下所示:

func TestDepositInvalid(t *testing.T) {
    account := Account{
        Customer: Customer{
            Name:    "John",
            Address: "Los Angeles, California",
            Phone:   "(213) 555 0147",
        },
        Number:  1001,
        Balance: 0,
    }

    if err := account.Deposit(-10); err == nil {
        t.Error("only positive numbers should be allowed to deposit")
    }
}

运行go test -v命令时,应该会看到测试通过:

=== RUN   TestAccount
--- PASS: TestAccount (0.00s)
=== RUN   TestDeposit
--- PASS: TestDeposit (0.00s)
=== RUN   TestDepositInvalid
--- PASS: TestDepositInvalid (0.00s)
PASS
ok      github.com/msft/bank    0.197s

实现取款方法

在编写取款功能之前,让我们为其编写测试:
func TestWithdraw(t *testing.T) {
    account := Account{
        Customer: Customer{
            Name:    "John",
            Address: "Los Angeles, California",
            Phone:   "(213) 555 0147",
        },
        Number:  1001,
        Balance: 0,
    }

    account.Deposit(10)
    account.Withdraw(10)

    if account.Balance != 0 {
        t.Error("balance is not being updated after withdraw")
    }
}

运行go test -v命令时,应该会在输出中看到一个将失败的测试:

# github.com/msft/bank [github.com/msft/bank.test]
./bank_test.go:67:9: account.Withdraw undefined (type Account has no field or method Withdraw)
FAIL    github.com/msft/bank [build failed]
让我们实现Withdraw方法的逻辑,该方法将帐户余额减少的金额就是以参数方式收到的金额。 像之前一样,我们需要验证收到的数字是否大于零,以及帐户中的余额是否足够。
将以下代码用于Withdraw方法:
// Withdraw ...
func (a *Account) Withdraw(amount float64) error {
    if amount <= 0 {
        return errors.New("the amount to withdraw should be greater than zero")
    }

    if a.Balance < amount {
        return errors.New("the amount to withdraw should be greater than the account's balance")
    }

    a.Balance -= amount
    return nil
}

运行go test -v命令时,应该会看到测试通过:

=== RUN   TestAccount
--- PASS: TestAccount (0.00s)
=== RUN   TestDeposit
--- PASS: TestDeposit (0.00s)
=== RUN   TestDepositInvalid
--- PASS: TestDepositInvalid (0.00s)
=== RUN   TestWithdraw
--- PASS: TestWithdraw (0.00s)
PASS
ok      github.com/msft/bank    0.250s

实现对账单方法

我们将编写一种方法来输出对账单,其中包含帐户名称、帐号和余额。 但是,首先让我们创建TestStatement函数:
func TestStatement(t *testing.T) {
    account := Account{
        Customer: Customer{
            Name:    "John",
            Address: "Los Angeles, California",
            Phone:   "(213) 555 0147",
        },
        Number:  1001,
        Balance: 0,
    }

    account.Deposit(100)
    statement := account.Statement()
    if statement != "1001 - John - 100" {
        t.Error("statement doesn't have the proper format")
    }
}

运行go test -v时,应该会在输出中看到一个将失败的测试:

# github.com/msft/bank [github.com/msft/bank.test]
./bank_test.go:86:22: account.Statement undefined (type Account has no field or method Statement)
FAIL    github.com/msft/bank [build failed]

让我们编写Statement方法,该方法应返回一个字符串。 (你稍后必须覆盖此方法,这是一项挑战。)使用以下代码:

// Statement ...
func (a *Account) Statement() string {
    return fmt.Sprintf("%v - %v - %v", a.Number, a.Name, a.Balance)
}

运行go test -v时,应该会看到测试通过:

=== RUN   TestAccount
--- PASS: TestAccount (0.00s)
=== RUN   TestDeposit
--- PASS: TestDeposit (0.00s)
=== RUN   TestDepositInvalid
--- PASS: TestDepositInvalid (0.00s)
=== RUN   TestWithdraw
--- PASS: TestWithdraw (0.00s)
=== RUN   TestStatement
--- PASS: TestStatement (0.00s)
PASS
ok      github.com/msft/bank    0.328s

接下来,请转到下一部分,编写Web API来公开Statement方法。

Go语言教程之边写边学:编写并测试程序 :开始编写测试
在开始编写程序之前,让我们先讨论一下测试并创建我们的第一个测试。 程序包测试为Go程序包的自动化测试提供支持。 测试对于确保代码按预期工作非常重要。 通常,程序包中的每个函数都应该有至少一个测试来确认功能。
编写代码时要遵循的一个良好做法是使用测试驱动开发 (TDD) 方法。 使用此方法时,我们将首先编写测试。 我们可以肯定那些测试会失败,因为它们测试的代码还不存在。 然后,我们将编写满足测试条件的代码。

创建测试文件

首先,我们需要创建用来保存bankcore程序包的所有测试的Go文件。 创建测试文件时,该文件的名称必须以 _test.go结尾。 你可以将你想用的任何内容用作文件名的前半部分,但典型做法是使用你要测试的文件的名称。
此外,要编写的每个测试都必须是以Test开头的函数。 然后,你通常为你编写的测试编写一个描述性名称,例如TestDeposit。
转到 $GOPATH/src/bankcore/ 位置,创建一个名为bank_test.go的文件,其中包含以下内容:
package bank

import "testing"

func TestAccount(t *testing.T) {

}

打开一个终端,确保你处于 $GOPATH/src/bankcore/ 位置。 然后,使用以下命令在详细模式下运行测试:

go test -v

Go将查找所有 *_test.go文件来运行测试,因此你应该会看到以下输出:

=== RUN   TestAccount
--- PASS: TestAccount (0.00s)
PASS
ok      github.com/msft/bank    0.391s

编写将失败的测试

编写任何代码之前,让我们先使用TDD为其编写一个将失败的测试。 使用以下代码修改TestAccount函数:
package bank

import "testing"

func TestAccount(t *testing.T) {
    account := Account{
        Customer: Customer{
            Name:    "John",
            Address: "Los Angeles, California",
            Phone:   "(213) 555 0147",
        },
        Number:  1001,
        Balance: 0,
    }

    if account.Name == "" {
        t.Error("can't create an Account object")
    }
}
我们引入了一个尚未实现的用于帐户和客户的结构。 并且,我们使用t.Error() 函数来指示,如果某件事情没有按预期的方式发生,测试将失败。
另请注意,测试具有创建帐户对象(尚不存在)的逻辑。 但是,我们此刻正在设计如何与我们的程序包进行交互。
运行go test -v命令时,应该会在输出中看到一个将失败的测试:
# github.com/msft/bank [github.com/msft/bank.test]
.\bank_test.go:6:13: undefined: Account
.\bank_test.go:7:13: undefined: Customer
FAIL    github.com/msft/bank [build failed]

让我们暂时先把它放在这里。 我们将完成此测试,并在为网上银行系统编写逻辑时创建新的测试。

Go语言教程之边写边学:编写并测试程序 :概述网上银行项目
我们来谈谈将要创建的内容。 如前所述,我们将创建两个项目:一个用于程序的核心逻辑,另一个用于通过Web API公开逻辑。 假设你现在是一个团队的一员,这个团队正在构建一个网上银行系统。

定义功能和要求

我们将要建立的网上银行是一个概念验证,它将确定构建银行程序是否可行。 在第一次迭代中,与核心程序包的交互将通过一个CLI程序进行。 我们没有用户界面,也不会将数据持久保存到数据库中。 为了查看客户的对账单,我们将公开一个终结点。

网上银行系统将:
允许客户创建帐户。
允许客户取款。
允许客户将资金转到其他帐户。
提供包含客户数据和最终余额的对账单。
通过终结点公开一个Web API,用于输出对账单。
我们将一同构建此程序,所以你现在不需要太担心细节。

创建初始项目文件

接下来,让我们创建程序所需的初始文件集。 我们将为所有银行核心逻辑和main程序创建一个Go程序包,以使用一些客户和操作(例如存款和转账)来初始化系统。 另外,此main程序还会启动一个Web API服务器,以便为对账单公开一个终结点。
让我们在 $GOPATH目录中创建以下文件结构:
$GOPATH/
  src/
    bankcore/
      go.mod
      bank.go
    bankapi/
      go.mod
      main.go
然后,为了确保我们只需要集中精力在合适的文件中编写代码,让我们开始编写一个Hello World! 程序,该程序将确认我们可以从bankapi主程序调用bankcore程序包。
将以下代码片段复制并粘贴到src/bankcore/bank.go中:
package bank

func Hello() string {
    return "Hey! I'm working!"
}

我们将使用Go模块。 在src/bankcore/go.mod中添加以下内容,为此程序包提供一个正确的名称,以便以后可以引用它:

module github.com/msft/bank

go 1.14

然后,在src/bankapi/main.go中添加以下代码来调用bankcore程序包:

package main

import (
    "fmt"

    "github.com/msft/bank"
)

func main() {
    fmt.Println(bank.Hello())
}

在src/bankapi/go.mod中,我们需要在本地引用bankcore程序包文件,如下所示:

module bankapi

go 1.14

require (
    github.com/msft/bank v0.0.1
)

replace github.com/msft/bank => ../bankcore

若要确保一切正常,请在 $GOPATH/src/bankapi/ 目录中打开终端并运行以下命令:

go run main.go

应会看到以下输出:

Hey! I'm working!

此输出确认你的项目文件已完全按预期正确设置。 接下来,我们将开始编写代码,以实现我们的网上银行系统的初始功能集。

Go语言教程之边写边学:了解go中并发工作原理:挑战:利用并发方法更快地计算斐波纳契数
对于这个挑战,你需要通过提高现有程序的运行速度来改进它。 尝试自己编写程序,即使你不得不回头查看你以前用于练习的示例也要尝试。 然后,将你的解决方案与下一单元中的解决方案进行比较。
Go中的并发是一个复杂的问题,在实践中你会更好地理解它。 这一挑战只是你可以用来实践的一个建议。
祝好运!

利用并发方法更快地计算斐波纳契数

使用以下程序按顺序计算斐波纳契数:
package main

import (
    "fmt"
    "math/rand"
    "time"
)

func fib(number float64) float64 {
    x, y := 1.0, 1.0
    for i := 0; i < int(number); i++ {
        x, y = y, x+y
    }

    r := rand.Intn(3)
    time.Sleep(time.Duration(r) * time.Second)

    return x
}

func main() {
    start := time.Now()

    for i := 1; i < 15; i++ {
        n := fib(float64(i))
    fmt.Printf("Fib(%v): %v\n", i, n)
    }

    elapsed := time.Since(start)
    fmt.Printf("Done! It took %v seconds!\n", elapsed.Seconds())
}
你需要根据现有代码构建两个程序:
实现并发的改进版本。 完成此操作需要几秒钟的时间(不超过15秒),就像现在这样。 应使用有缓冲channel。
编写一个新版本以计算斐波纳契数,直到用户使用fmt.Scanf() 函数在终端中输入quit。 如果用户按Enter,则应计算新的斐波纳契数。 换句话说,你将不再有从1到10的循环。
使用两个无缓冲channel:一个用于计算斐波纳契数,另一个用于等待用户的"退出"消息。 你需要使用select语句。
下面是与程序进行交互的示例:
1

1

2

3

5

8

13
quit
Done calculating Fibonacci!
Done! It took 12.043196415 seconds!

实现并发并使程序的运行速度更快的改进版本如下所示:

package main

import (
    "fmt"
    "math/rand"
    "time"
)

func fib(number float64, ch chan string) {
    x, y := 1.0, 1.0
    for i := 0; i < int(number); i++ {
        x, y = y, x+y
    }

    r := rand.Intn(3)
    time.Sleep(time.Duration(r) * time.Second)

    ch <- fmt.Sprintf("Fib(%v): %v\n", number, x)
}

func main() {
    start := time.Now()

    size := 15
    ch := make(chan string, size)

    for i := 0; i < size; i++ {
        go fib(float64(i), ch)
    }

    for i := 0; i < size; i++ {
        fmt.Printf(<-ch)
    }

    elapsed := time.Since(start)
    fmt.Printf("Done! It took %v seconds!\n", elapsed.Seconds())
}

输出:

Fib(14): 610
Fib(8): 34
Fib(1): 1
Fib(5): 8
Fib(0): 1
Fib(12): 233
Fib(2): 2
Fib(13): 377
Fib(6): 13
Fib(7): 21
Fib(4): 5
Fib(3): 3
Fib(10): 89
Fib(9): 55
Fib(11): 144
Done! It took 2 seconds!

使用两个无缓冲channel的程序的第二个版本如下所示:

package main

import (
    "fmt"
    "time"
)

var quit = make(chan bool)

func fib(c chan int) {
    x, y := 1, 1

    for {
        select {
            case c <- x:
                x, y = y, x+y
            case <-quit:
                fmt.Println("Done calculating Fibonacci!")
            return
        }
    }
}

func main() {
    start := time.Now()

    command := ""
    data := make(chan int)

    go fib(data)

    for {
        num := <-data
        fmt.Println(num)
        fmt.Scanf("%s", &command)
        if num > 10000 {
            quit <- true
            break
        }
    }

    time.Sleep(1 * time.Second)

    elapsed := time.Since(start)
    fmt.Printf("Done! It took %v seconds!\n", elapsed.Seconds())
}

输出:

1
1
2
3
5
8
13
21
34
55
89
144
233
377
610
987
1597
2584
4181
6765
10946
Done calculating Fibonacci!
Done! It took 1 seconds!
Go语言教程之边写边学:了解go中并发工作原理:了解有缓冲channel
正如你所了解的,默认情况下channel是无缓冲行为。 这意味着只有存在接收操作时,它们才接受发送操作。 否则,程序将永久被阻止等待。
有时需要在goroutine之间进行此类同步。 但是,有时你可能只需要实现并发,而不需要限制goroutine之间的通信方式。
有缓冲channel在不阻止程序的情况下发送和接收数据,因为有缓冲channel的行为类似于队列。 创建channel时,可以限制此队列的大小,如下所示:
ch := make(chan string, 10)
每次向channel发送数据时,都会将元素添加到队列中。 然后,接收操作将从队列中删除该元素。 当channel已满时,任何发送操作都将等待,直到有空间保存数据。 相反,如果channel是空的且存在读取操作,程序则会被阻止,直到有数据要读取。
下面是一个理解有缓冲channel的简单示例:
package main

import (
    "fmt"
)

func send(ch chan string, message string) {
    ch <- message
}

func main() {
    size := 4
    ch := make(chan string, size)
    send(ch, "one")
    send(ch, "two")
    send(ch, "three")
    send(ch, "four")
    fmt.Println("All data sent to the channel ...")

    for i := 0; i < size; i++ {
        fmt.Println(<-ch)
    }

    fmt.Println("Done!")
}

运行程序时,将看到以下输出:

All data sent to the channel ...
one
two
three
four
Done!

你可能会说我们在这里没有做任何不同的操作,你是对的。 但是让我们看看当你将size变量更改为一个更小的数字(你甚至可以尝试使用一个更大的数字)时会发生什么情况,如下所示:

size := 2

重新运行程序时,将看到以下错误:

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.send(...)
        /Users/developer/go/src/concurrency/main.go:8
main.main()
        /Users/developer/go/src/concurrency/main.go:16 +0xf3
exit status 2
出现此错误是因为对send函数的调用是连续的。 你不是在创建新的goroutine。 因此,没有任何要排队的操作。
channel与goroutine有着紧密的联系。 如果没有另一个goroutine从channel接收数据,则整个程序可能会永久处于被阻止状态。 正如你所见,这种情况确实会发生。
现在让我们进行一些有趣的实践! 我们将为最后两次调用创建goroutine (前两次调用正确适应缓冲区),并运行for循环四次。 代码如下:
func main() {
    size := 2
    ch := make(chan string, size)
    send(ch, "one")
    send(ch, "two")
    go send(ch, "three")
    go send(ch, "four")
    fmt.Println("All data sent to the channel ...")

    for i := 0; i < 4; i++ {
        fmt.Println(<-ch)
    }

    fmt.Println("Done!")
}
运行程序时,它按预期工作。 我们建议在使用channel时始终使用goroutine。
让我们测试一下创建的缓冲通道的元素超出所需量的情况。 我们将使用之前用于检查API的示例,并创建大小为10的缓冲通道:
package main

import (
    "fmt"
    "net/http"
    "time"
)

func main() {
    start := time.Now()

    apis := []string{
        "https://management.azure.com",
        "https://dev.azure.com",
        "https://api.github.com",
        "https://outlook.office.com/",
        "https://api.somewhereintheinternet.com/",
        "https://graph.microsoft.com",
    }

    ch := make(chan string, 10)

    for _, api := range apis {
        go checkAPI(api, ch)
    }

    for i := 0; i < len(apis); i++ {
        fmt.Print(<-ch)
    }

    elapsed := time.Since(start)
    fmt.Printf("Done! It took %v seconds!\n", elapsed.Seconds())
}

func checkAPI(api string, ch chan string) {
    _, err := http.Get(api)
    if err != nil {
        ch <- fmt.Sprintf("ERROR: %s is down!\n", api)
        return
    }

    ch <- fmt.Sprintf("SUCCESS: %s is up and running!\n", api)
}
运行程序时,将看到与以前相同的输出。 可以更改channel的大小,用更小或更大的数字进行测试,程序仍能正常运行。

无缓冲channel与有缓冲channel

现在,你可能想知道何时使用这两种类型。 这完全取决于你希望goroutine之间的通信如何进行。 无缓冲channel同步通信。 它们保证每次发送数据时,程序都会被阻止,直到有人从channel中读取数据。
相反,有缓冲channel将发送和接收操作解耦。 它们不会阻止程序,但你必须小心使用,因为可能最终会导致死锁(如前文所述)。 使用无缓冲channel时,可以控制可并发运行的goroutine的数量。 例如,你可能要对API进行调用,并且想要控制每秒执行的调用次数。 否则,你可能会被阻止。

Channel方向

Go中的通道具有另一个有趣的功能。 在使用通道作为函数的参数时,可以指定通道是要"发送"数据还是"接收"数据。 随着程序的增长,可能会使用大量的函数,这时候,最好记录每个channel的意图,以便正确使用它们。 或者,你要编写一个库,并希望将channel公开为只读,以保持数据一致性。
要定义channel的方向,可以使用与读取或接收数据时类似的方式进行定义。 但是你在函数参数中声明channel时执行此操作。 将通道类型定义为函数中的参数的语法如下所示:
chan<- int // 仅用于写入数据的chan
<-chan int // 仅用于读取数据的chan
通过仅接收的channel发送数据时,在编译程序时会出现错误。
让我们使用以下程序作为两个函数的示例,一个函数用于读取数据,另一个函数用于发送数据:
package main

import "fmt"

func send(ch chan<- string, message string) {
    fmt.Printf("Sending: %#v\n", message)
    ch <- message
}

func read(ch <-chan string) {
    fmt.Printf("Receiving: %#v\n", <-ch)
}

func main() {
    ch := make(chan string, 1)
    send(ch, "Hello World!")
    read(ch)
}

运行程序时,将看到以下输出:

Sending: "Hello World!"
Receiving: "Hello World!"

程序阐明每个函数中每个channel的意图。 如果试图使用一个channel在一个仅用于接收数据的channel中发送数据,将会出现编译错误。 例如,尝试执行如下所示的操作:

func read(ch <-chan string) {
    fmt.Printf("Receiving: %#v\n", <-ch)
    ch <- "Bye!"
}

运行程序时,将看到以下错误:

# command-line-arguments
./main.go:12:5: invalid operation: ch <- "Bye!" (send to receive-only type <-chan string)

编译错误总比误用channel好。

多路复用

最后,让我们讨论如何使用select关键字与多个通道同时交互。 有时,在使用多个channel时,需要等待事件发生。 例如,当程序正在处理的数据中出现异常时,可以包含一些逻辑来取消操作。
select语句的工作方式类似于switch语句,但它适用于channel。 它会阻止程序的执行,直到它收到要处理的事件。 如果它收到多个事件,则会随机选择一个。
select语句的一个重要方面是,它在处理事件后完成执行。 如果要等待更多事件发生,则可能需要使用循环。
让我们使用以下程序来看看select的运行情况:
package main

import (
    "fmt"
    "time"
)

func process(ch chan string) {
    time.Sleep(3 * time.Second)
    ch <- "Done processing!"
}

func replicate(ch chan string) {
    time.Sleep(1 * time.Second)
    ch <- "Done replicating!"
}

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)
    go process(ch1)
    go replicate(ch2)

    for i := 0; i < 2; i++ {
        select {
        case process := <-ch1:
            fmt.Println(process)
        case replicate := <-ch2:
            fmt.Println(replicate)
        }
    }
}

运行程序时,将看到以下输出:

Done replicating!
Done processing!

请注意,replicate函数首先完成,这就是首先在终端中看到其输出的原因。 main函数存在一个循环,因为select语句在收到事件后立即结束,但我们仍在等待process函数完成。

Go语言教程之边写边学:了解go中并发工作原理:将channel用作通信机制
Go中的channel是goroutine之间的通信机制。 请记住Go的并发方法是:"不是通过共享内存通信;而是通过通信共享内存。"当你需要将值从一个goroutine发送到另一个时,可以使用通道。 让我们看看它们的工作原理,以及如何开始使用它们来编写并发Go程序。

Channel语法

由于channel是发送和接收数据的通信机制,因此它也有类型之分。 这意味着你只能发送channel支持的数据类型。 除使用关键字chan作为channel的数据类型外,还需指定将通过channel传递的数据类型,如int类型。
每次声明一个channel或希望在函数中指定一个channel作为参数时,都需要使用chan <type>,如chan int。 若要创建通道,需使用内置的make() 函数:
ch := make(chan int)
一个channel可以执行两项操作:发送数据和接收数据。 若要指定channel具有的操作类型,需要使用channel运算符 <-。 此外,在channel中发送数据和接收数据属于阻止操作。 你一会儿就会明白为何如此。
如果希望通道仅发送数据,请在通道之后使用 <- 运算符。 如果希望通道接收数据,请在通道之前使用 <- 运算符,如下所示:
ch <- x // 向ch写入数据x = <-ch // 从ch读取数据
<-ch // 从ch读取数据但不做任何处理

可在channel中执行的另一项操作是关闭channel。 若要关闭通道,使用内置的close() 函数:

close(ch)
当你关闭通道时,你希望数据将不再在该通道中发送。 如果试图将数据发送到已关闭的channel,则程序将发生严重错误。 如果试图通过已关闭的channel接收数据,则可以读取发送的所有数据。 随后的每次"读取"都将返回一个零值。
让我们回到之前创建的程序,然后使用通道来删除睡眠功能。 首先,让我们在main函数中创建一个字符串channel,如下所示:
ch := make(chan string)
接下来,删除睡眠行time.Sleep(3 * time.Second)。
现在,我们可以使用channel在goroutine之间进行通信。 应重构代码并通过通道发送该消息,而不是在checkAPI函数中打印结果。 要使用该函数中的channel,需要添加channel作为参数。 checkAPI函数应如下所示:
func checkAPI(api string, ch chan string) {
    _, err := http.Get(api)
    if err != nil {
        ch <- fmt.Sprintf("ERROR: %s is down!\n", api)
        return
    }

    ch <- fmt.Sprintf("SUCCESS: %s is up and running!\n", api)
}
请注意,我们必须使用fmt.Sprintf函数,因为我们不想打印任何文本,只需利用通道发送格式化文本。 另请注意,我们在channel变量之后使用 <- 运算符来发送数据。
现在,你需要更改main函数以发送channel变量并接收要打印的数据,如下所示:
ch := make(chan string)

for _, api := range apis {
    go checkAPI(api, ch)
}

fmt.Print(<-ch)
请注意,我们在channel之前使用 <- 运算符来表明我们想要从channel读取数据。
重新运行程序时,会看到如下所示的输出:
ERROR: https://api.somewhereintheinternet.com/ is down!

Done! It took 0.007401217 seconds!
至少它不用调用睡眠函数就可以工作,对吧? 但它仍然没有达到我们的目的。 我们只看到其中一个goroutine的输出,而我们共创建了五个goroutine。 在下一节中,我们来看看这个程序为什么是这样工作的。

无缓冲channel

使用make() 函数创建channel时,会创建一个无缓冲channel,这是默认行为。 无缓冲channel会阻止发送操作,直到有人准备好接收数据。 正如我们之前所说,发送和接收都属于阻止操作。 此阻止操作也是上一节中的程序在收到第一条消息后立即停止的原因。
我们可以说fmt.Print(<-ch) 会阻止程序,因为它从channel读取,并等待一些数据到达。 一旦有任何数据到达,它就会继续下一行,然后程序完成。
其他goroutine发生了什么? 它们仍在运行,但都没有在侦听。 而且,由于程序提前完成,一些goroutine无法发送数据。 为了证明这一点,让我们添加另一个fmt.Print(<-ch),如下所示:
ch := make(chan string)

for _, api := range apis {
    go checkAPI(api, ch)
}

fmt.Print(<-ch)
fmt.Print(<-ch)

重新运行程序时,会看到如下所示的输出:

ERROR: https://api.somewhereintheinternet.com/ is down!
SUCCESS: https://api.github.com is up and running!
Done! It took 0.263611711 seconds!

请注意,现在你会看到两个API的输出。 如果继续添加更多fmt.Print(<-ch) 行,你最终将会读取发送到channel的所有数据。 但是如果你试图读取更多数据,而没有goroutine再发送数据,会发生什么呢? 例如:

ch := make(chan string)

for _, api := range apis {
    go checkAPI(api, ch)
}

fmt.Print(<-ch)
fmt.Print(<-ch)
fmt.Print(<-ch)
fmt.Print(<-ch)
fmt.Print(<-ch)
fmt.Print(<-ch)

fmt.Print(<-ch)

重新运行程序时,会看到如下所示的输出:

ERROR: https://api.somewhereintheinternet.com/ is down!
SUCCESS: https://api.github.com is up and running!
SUCCESS: https://management.azure.com is up and running!
SUCCESS: https://graph.microsoft.com is up and running!
SUCCESS: https://outlook.office.com/ is up and running!
SUCCESS: https://dev.azure.com is up and running!
它在运行,但程序未完成。 最后一个打印行阻止了程序,因为它需要接收数据。 必须使用类似Ctrl+C的命令关闭程序。
上个示例只是证明了读取数据和接收数据都属于阻止操作。 要解决此问题,可以将代码更改为for循环,并只接收确定要发送的数据,如下所示:
for i := 0; i < len(apis); i++ {
    fmt.Print(<-ch)
}

以下是程序的最终版本,以防你的版本出错:

package main

import (
    "fmt"
    "net/http"
    "time"
)

func main() {
    start := time.Now()

    apis := []string{
        "https://management.azure.com",
        "https://dev.azure.com",
        "https://api.github.com",
        "https://outlook.office.com/",
        "https://api.somewhereintheinternet.com/",
        "https://graph.microsoft.com",
    }

    ch := make(chan string)

    for _, api := range apis {
        go checkAPI(api, ch)
    }

    for i := 0; i < len(apis); i++ {
        fmt.Print(<-ch)
    }

    elapsed := time.Since(start)
    fmt.Printf("Done! It took %v seconds!\n", elapsed.Seconds())
}

func checkAPI(api string, ch chan string) {
    _, err := http.Get(api)
    if err != nil {
        ch <- fmt.Sprintf("ERROR: %s is down!\n", api)
        return
    }

    ch <- fmt.Sprintf("SUCCESS: %s is up and running!\n", api)
}

重新运行程序时,会看到如下所示的输出:

ERROR: https://api.somewhereintheinternet.com/ is down!
SUCCESS: https://api.github.com is up and running!
SUCCESS: https://management.azure.com is up and running!
SUCCESS: https://dev.azure.com is up and running!
SUCCESS: https://graph.microsoft.com is up and running!
SUCCESS: https://outlook.office.com/ is up and running!
Done! It took 0.602099714 seconds!
程序正在执行应执行的操作。 你不再使用睡眠函数,而是使用通道。 另请注意,在不使用并发时,现在需要约600毫秒完成,而不会耗费近2秒。
最后,我们可以说,无缓冲channel在同步发送和接收操作。 即使使用并发,通信也是同步的。
Go语言教程之边写边学:了解go中并发工作原理:了解goroutine
并发是独立活动的组合,就像Web服务器虽然同时处理多个用户请求,但它是自主运行的。 并发在当今的许多程序中都存在。 Web服务器就是一个例子,但你也能看到,在批量处理大量数据时也需要使用并发。
Go有两种编写并发程序的样式。 一种是在其他语言中通过线程实现的传统样式。 在本模块中,你将了解Go的样式,其中值是在称为goroutine的独立活动之间传递的,以与进程进行通信。
如果这是你第一次学习并发,我们建议你多花一些时间来查看我们将要编写的每一段代码,以进行实践。

Go实现并发的方法

通常,编写并发程序时最大的问题是在进程之间共享数据。 Go采用不同于其他编程语言的通信方式,因为Go是通过channel来回传递数据的。 此方法意味着只有一个活动 (goroutine) 有权访问数据,设计上不存在争用条件。 学完本模块中的goroutine和channel之后,你将更好地理解Go的并发方法。
Go的方法可以总结为以下口号:"不是通过共享内存通信,而是通过通信共享内存。"我们将在以下部分介绍此方法,但你也可以在Go博客文章通过通信共享内存中了解详细信息。
如前所述,Go还提供低级别的并发基元。 但在本模块中,我们只介绍Go的惯用并发方法。
让我们从探索goroutine开始。

Goroutine

goroutine是轻量线程中的并发活动,而不是在操作系统中进行的传统活动。 假设你有一个写入输出的程序和另一个计算两个数字相加的函数。 一个并发程序可以有数个goroutine同时调用这两个函数。
我们可以说,程序执行的第一个goroutine是main() 函数。 如果要创建其他goroutine,则必须在调用该函数之前使用go关键字,如下所示:
func main(){
    login()
    go launch()
}

你还会发现,许多程序喜欢使用匿名函数来创建goroutine,如此代码中所示:

func main(){
    login()
    go func() {
        launch()
    }()
}

为了查看运行中的goroutine,让我们编写一个并发程序。

编写并发程序

由于我们只想将重点放在并发部分,因此我们使用现有程序来检查API终结点是否响应。 代码如下:
package main

import (
    "fmt"
    "net/http"
    "time"
)

func main() {
    start := time.Now()

    apis := []string{
        "https://management.azure.com",
        "https://dev.azure.com",
        "https://api.github.com",
        "https://outlook.office.com/",
        "https://api.somewhereintheinternet.com/",
        "https://graph.microsoft.com",
    }

    for _, api := range apis {
        _, err := http.Get(api)
        if err != nil {
            fmt.Printf("ERROR: %s is down!\n", api)
            continue
        }

        fmt.Printf("SUCCESS: %s is up and running!\n", api)
    }

    elapsed := time.Since(start)
    fmt.Printf("Done! It took %v seconds!\n", elapsed.Seconds())
}

运行前面的代码时,将看到以下输出:

SUCCESS: https://management.azure.com is up and running!
SUCCESS: https://dev.azure.com is up and running!
SUCCESS: https://api.github.com is up and running!
SUCCESS: https://outlook.office.com/ is up and running!
ERROR: https://api.somewhereintheinternet.com/ is down!
SUCCESS: https://graph.microsoft.com is up and running!
Done! It took 1.658436834 seconds!
这里没有什么特别之处,但我们可以做得更好。 或许我们可以同时检查所有站点? 此程序可以在500毫秒的时间内完成,不需要耗费将近两秒。
请注意,我们需要并发运行的代码部分是向站点进行HTTP调用的部分。 换句话说,我们需要为程序要检查的每个API创建一个goroutine。
为了创建goroutine,我们需要在调用函数前使用go关键字。 但我们在这里没有函数。 让我们重构该代码并创建一个新函数,如下所示:
func checkAPI(api string) {
    _, err := http.Get(api)
    if err != nil {
        fmt.Printf("ERROR: %s is down!\n", api)
        return
    }

    fmt.Printf("SUCCESS: %s is up and running!\n", api)
}

注意,我们不再需要continue关键字,因为我们不在for循环中。 要停止函数的执行流,只需使用return关键字。 现在,我们需要修改main() 函数中的代码,为每个API创建一个goroutine,如下所示:

for _, api := range apis {
    go checkAPI(api)
}
重新运行程序,看看发生了什么。
看起来程序不再检查API了,对吗? 显示的内容可能与以下输出类似:
Done! It took 1.506e-05 seconds!
速度可真快! 发生了什么情况? 你会看到最后一条消息,指出程序已完成,因为Go为循环中的每个站点创建了一个goroutine,并且它立即转到下一行。
即使看起来checkAPI函数没有运行,它实际上是在运行。 它只是没有时间完成。 请注意,如果在循环之后添加一个睡眠计时器会发生什么,如下所示:
for _, api := range apis {
    go checkAPI(api)
}

time.Sleep(3 * time.Second)
现在,重新运行程序时,可能会看到如下所示的输出:
ERROR: https://api.somewhereintheinternet.com/ is down!
SUCCESS: https://api.github.com is up and running!
SUCCESS: https://management.azure.com is up and running!
SUCCESS: https://dev.azure.com is up and running!
SUCCESS: https://outlook.office.com/ is up and running!
SUCCESS: https://graph.microsoft.com is up and running!
Done! It took 3.002114573 seconds!
看起来似乎起作用了,对吧? 不完全如此。 如果你想在列表中添加一个新站点呢? 也许三秒钟是不够的。 你怎么知道? 你无法管理。 必须有更好的方法,这就是我们在下一节讨论channel时要涉及的内容。
Go语言教程之边写边学:数据结构与算法:LZW 数据无损压缩和解压缩
1、LZW算法的基本概念
LZW有三个重要对象:数据流(CharStream)、编码流(String Table)和编译表(String Table)。
(1)编码时,数据流是输入对象 (数据序列),编码流就是输出对象(经过压缩运算的编码数据);
(2)解码时,编码流是输入对象,数据流是输出对象;而编译表是在编码和解码时都需要借助的对象。
2、LZW压缩的基本原理
提取原始文本文件数据中的不同字符,基于这些字符创建一个编译表,然后用编译表中的字符的索引来替代原始数据中的相应字符,减少原始数据大小。注意:此处的编译表是根据原始数据动态创建的,解码时需要从已编码的数据中还原出原来的编译表。
3LZW算法流程
步骤一:开始时词典包含所有可能的根,当前前缀字符串P和 当前字符 均为空;
步骤二:读入新的字符C,与P合并形成字符串P+C;
步骤三:判断P+C是否在字典中
                        如果"是":
                                P = P + C; 
                                返回步骤二;
                        如果"否":
                                输出P的映射;
                                 P = P+C ;
                                把前缀字符串P添加到字典,建立映射;
                                令P = C //(现在的P仅包含一个字符C);
步骤四: 判断码字流中是否还有码字要译
                         如果"是":
                                 返回步骤二;
                         如果"否":
                            把代表当前前缀P的码字输出到码字流;
                            结束。


实现代码:

package main
import "fmt"
 

func compressLZW(testStr string) []int {    
    code := 256
    dictionary := make(map[string]int)
    for i := 0; i < 256; i++ {
        dictionary[string(i)] = i
    }
 
    currChar := ""
    result := make([]int, 0)	
    for _, c := range []byte(testStr) {	
        phrase := currChar + string(c)
        if _, isTrue := dictionary[phrase]; isTrue {		
            currChar = phrase
        } else {
            result = append(result, dictionary[currChar])
            dictionary[phrase] = code
            code++
            currChar = string(c)
        }
    }
    if currChar != "" {
        result = append(result, dictionary[currChar])
    }
    return result
}
 

func decompressLZW(compressed []int) string {    
    code := 256
    dictionary := make(map[int]string)
    for i := 0; i < 256; i++ {
        dictionary[i] = string(i)
    }
 
    currChar := string(compressed[0])
    result := currChar
    for _, element := range compressed[1:] {
        var word string
        if x, ok := dictionary[element]; ok {
            word = x
        } else if element == code {
            word = currChar + currChar[:1]
        } else {
            panic(fmt.Sprintf("Bad compressed element: %d", element))
        }
 
        result += word
        
        dictionary[code] = currChar + word[:1]
        code++
 
        currChar = word
    }
    return result
}
 
func main() {
	fmt.Print("Enter any string :")
	var testStr string
    fmt.Scanln(&testStr)
	
    compressed := compressLZW(testStr)
    fmt.Println("\nAfter Compression :", compressed)
	
    uncompression := decompressLZW(compressed)
    fmt.Println("\nAfter Uncompression :", uncompression)
}

输出:

Enter any string :Australia
After Compression : [65 117 115 116 114 97 108 105 97]

After Uncompression : Australia

C:\golang\example>go run test.go
Enter any string :Germany

After Compression : [71 101 114 109 97 110 121]

After Uncompression : Germany
Go语言教程之边写边学:数据结构与算法:打印给定字符串的所有排列

排列是将有序列表S的元素重新排列为与S本身一一对应的对应关系。长度为n的字符串有n!排列。让我们以"ABCD"为例,编写一个程序来生成Golang中所有可能的字符串排列和组合。

实现代码:

package main

import (
    "fmt"
)

func join(ins []rune, c rune) (res []string) {
    for i := 0; i <= len(ins); i++ {
        res = append(res, string(ins[:i])+string(c)+string(ins[i:]))
    }
    return res
}

func permutations(testStr string) []string {
    var n func(testStr []rune, p []string) []string
    n = func(testStr []rune, p []string) []string {
        if len(testStr) == 0 {
            return p
        } else {
            result := []string{}
            for _, e := range p {
                result = append(result, join([]rune(e), testStr[0])...)
            }
            return n(testStr[1:], result)
        }
    }

    output := []rune(testStr)
    return n(output[1:], []string{string(output[0])})
}

func main() {
    d := permutations("ABCD")
    fmt.Println(d)
}

输出:

[DCBA CDBA CBDA CBAD DBCA BDCA BCDA BCAD DBAC BDAC BADC BACD DCAB CDAB CADB CABD DACB ADCB ACDB ACBD DABC ADBC ABDC ABCD]
Go语言教程之边写边学:数据结构与算法:生成自平衡二叉查找树 AVL tree

AVL树是高度平衡的二叉搜索树。AVL树检查左侧和右侧子树的高度,并确保差异不超过1,这种差异称为平衡因子。

实现代码:

package main
 
import (
    "encoding/json"
    "fmt"
)

type Key interface {
    Less(Key) bool
    Eq(Key) bool
}
 

type Node struct {
    Data    Key
    Balance int
    Link    [2]*Node
}
 

func opp(dir int) int {
    return 1 - dir
}
 
// single rotation
func single(root *Node, dir int) *Node {
    save := root.Link[opp(dir)]
    root.Link[opp(dir)] = save.Link[dir]
    save.Link[dir] = root
    return save
}
 
// double rotation
func double(root *Node, dir int) *Node {
    save := root.Link[opp(dir)].Link[dir]
 
    root.Link[opp(dir)].Link[dir] = save.Link[opp(dir)]
    save.Link[opp(dir)] = root.Link[opp(dir)]
    root.Link[opp(dir)] = save
 
    save = root.Link[opp(dir)]
    root.Link[opp(dir)] = save.Link[dir]
    save.Link[dir] = root
    return save
}
 
// adjust valance factors after double rotation
func adjustBalance(root *Node, dir, bal int) {
    n := root.Link[dir]
    nn := n.Link[opp(dir)]
    switch nn.Balance {
    case 0:
        root.Balance = 0
        n.Balance = 0
    case bal:
        root.Balance = -bal
        n.Balance = 0
    default:
        root.Balance = 0
        n.Balance = bal
    }
    nn.Balance = 0
}
 
func insertBalance(root *Node, dir int) *Node {
    n := root.Link[dir]
    bal := 2*dir - 1
    if n.Balance == bal {
        root.Balance = 0
        n.Balance = 0
        return single(root, opp(dir))
    }
    adjustBalance(root, dir, bal)
    return double(root, opp(dir))
}
 
func insertR(root *Node, data Key) (*Node, bool) {
    if root == nil {
        return &Node{Data: data}, false
    }
    dir := 0
    if root.Data.Less(data) {
        dir = 1
    }
    var done bool
    root.Link[dir], done = insertR(root.Link[dir], data)
    if done {
        return root, true
    }
    root.Balance += 2*dir - 1
    switch root.Balance {
    case 0:
        return root, true
    case 1, -1:
        return root, false
    }
    return insertBalance(root, dir), true
}
 
// Insert a node into the AVL tree.
func Insert(tree **Node, data Key) {
    *tree, _ = insertR(*tree, data)
}

// Remove a single item from an AVL tree.
func Remove(tree **Node, data Key) {
    *tree, _ = removeR(*tree, data)
}

func removeBalance(root *Node, dir int) (*Node, bool) {
    n := root.Link[opp(dir)]
    bal := 2*dir - 1
    switch n.Balance {
    case -bal:
        root.Balance = 0
        n.Balance = 0
        return single(root, dir), false
    case bal:
        adjustBalance(root, opp(dir), -bal)
        return double(root, dir), false
    }
    root.Balance = -bal
    n.Balance = bal
    return single(root, dir), true
}
 
func removeR(root *Node, data Key) (*Node, bool) {
    if root == nil {
        return nil, false
    }
    if root.Data.Eq(data) {
        switch {
        case root.Link[0] == nil:
            return root.Link[1], false
        case root.Link[1] == nil:
            return root.Link[0], false
        }
        heir := root.Link[0]
        for heir.Link[1] != nil {
            heir = heir.Link[1]
        }
        root.Data = heir.Data
        data = heir.Data
    }
    dir := 0
    if root.Data.Less(data) {
        dir = 1
    }
    var done bool
    root.Link[dir], done = removeR(root.Link[dir], data)
    if done {
        return root, true
    }
    root.Balance += 1 - 2*dir
    switch root.Balance {
    case 1, -1:
        return root, true
    case 0:
        return root, false
    }
    return removeBalance(root, dir)
}
 

type intKey int 
func (k intKey) Less(k2 Key) bool { return k < k2.(intKey) }
func (k intKey) Eq(k2 Key) bool   { return k == k2.(intKey) }
 
func main() {
    var tree *Node
    fmt.Println("Empty Tree:")    
	avl,_ := json.MarshalIndent(tree, "", "   ")
	fmt.Println(string(avl))

    fmt.Println("\nInsert Tree:")
    Insert(&tree, intKey(4))
    Insert(&tree, intKey(2))
    Insert(&tree, intKey(7))
    Insert(&tree, intKey(6))
	Insert(&tree, intKey(6))
    Insert(&tree, intKey(9))
    avl,_ = json.MarshalIndent(tree, "", "   ")
	fmt.Println(string(avl))
 
    fmt.Println("\nRemove Tree:")
    Remove(&tree, intKey(4))
    Remove(&tree, intKey(6))
    avl,_ = json.MarshalIndent(tree, "", "   ")
	fmt.Println(string(avl))
}

输出:

Empty Tree:
null
Insert Tree:
{
   "Data": 6,
   "Balance": 0,
   "Link": [
      {
         "Data": 4,
         "Balance": 0,
         "Link": [
            {
               "Data": 2,
               "Balance": 0,
               "Link": [
                  null,
                  null
               ]
            },
            {
               "Data": 6,
               "Balance": 0,
               "Link": [
                  null,
                  null
               ]
            }
         ]
      },
      {
         "Data": 7,
         "Balance": 1,
         "Link": [
            null,
            {
               "Data": 9,
               "Balance": 0,
               "Link": [
                  null,
                  null
               ]
            }
         ]
      }
   ]
}
Remove Tree:
{
   "Data": 6,
   "Balance": 1,
   "Link": [
      {
         "Data": 2,
         "Balance": 0,
         "Link": [
            null,
            null
         ]
      },
      {
         "Data": 7,
         "Balance": 1,
         "Link": [
            null,
            {
               "Data": 9,
               "Balance": 0,
               "Link": [
                  null,
                  null
               ]
            }
         ]
      }
   ]
}
Go语言教程之边写边学:数据结构与算法:生成数字螺旋矩阵

绘制2D矩阵,以螺旋格式打印给定矩阵的所有元素。给定一个数字n,使用O(1) 空间顺时针方向打印n x n螺旋矩阵(从1到n x n的数字)。

实现代码:

package main
  
import (
    "fmt"
)
  
func spiral(n int) []int {
    left, top, right, bottom := 0, 0, n-1, n-1
    sz := n * n
    s := make([]int, sz)
    i := 0
    for left < right {
        // work right, along top
        for c := left; c <= right; c++ {
            s[top*n+c] = i
            i++
        }
        top++
        // work down right side
        for r := top; r <= bottom; r++ {
            s[r*n+right] = i
            i++
        }
        right--
        if top == bottom {
            break
        }
        // work left, along bottom
        for c := right; c >= left; c-- {
            s[bottom*n+c] = i
            i++
        }
        bottom--
        // work up left side
        for r := bottom; r >= top; r-- {
            s[r*n+left] = i
            i++
        }
        left++
    }
    // center (last) element
    s[top*n+left] = i
  
    return s
}
  
func main() {
    num := 5
    len := 2
    for i, draw := range spiral(num) {
        fmt.Printf("%*d ", len, draw)
        if i%num == num-1 {
            fmt.Println("")
        }
    }
}

输出:

0  1  2  3  4 
15 16 17 18  5 
14 23 24 19  6 
13 22 21 20  7 
12 11 10  9  8 
  • 当前日期:
  • 北京时间:
  • 时间戳:
  • 今年的第:17周
  • 我的 IP:3.134.105.118
农历
五行
冲煞
彭祖
方位
吉神
凶神
极简任务管理 help
+ 0 0 0
Task Idea Collect