• Java程序员的Golang入门指南(下)


    Java程序员的Golang入门指南(下)

    4.高级特性

    上面介绍的只是Golang的基本语法和特性,尽管像控制语句的条件不用圆括号、函数多返回值、switch-case默认break、函数闭包、集合切片等特性相比Java的确提高了开发效率,但这些在其他语言中也都有,并不是Golang能真正吸引人的地方。不仅是Golang,我们学习任何语言当然都是从基本语法特性着手,但学习时要不断地问自己:使这门语言区别于其他语言的”独到之处“在哪?这种独到之处往往反映了语言的设计思想、出发点、要解决的”痛点“,这才是一门语言或任何技术的立足之本

    4.1 goroutine

    goroutine使用go关键字来调用函数,也可以使用匿名函数。可以简单的把go关键字调用的函数想像成pthread_create。如果一个goroutine没有被阻塞,那么别的goroutine就不会得到执行。也就是说goroutine阻塞时,Golang会切换到其他goroutine执行,这是非常好的特性!Java对类似goroutine这种的协程没有原生支持,像Akka最害怕的就是阻塞。因为协程不等同于线程,操作系统不会帮我们完成“现场”保存和恢复,所以要实现goroutine这种特性,就要模拟操作系统的行为,保存方法或函数在协程“上下文切换”时的Context,当阻塞结束时才能正确地切换回来。像Kilim等协程库利用字节码生成,能够胜任,而Akka完全是运行时的。

    注意:如果你要真正的并发,需要调用runtime.GOMAXPROCS(CPU_NUM)设置。

    package main
    
    import "fmt"
    
    func main() {
        go f("goroutine")
    
        go func(msg string) {
            fmt.Println(msg)
        }("going")
    
        // Block main thread
        var input string
        fmt.Scanln(&input)
        fmt.Println("done")
    }
    
    func f(msg string) {
        fmt.Println(msg)
    }

    4.2 原子操作

    像Java一样,Golang支持很多CAS操作。运行结果是unsaftCnt可能小于200,因为unsafeCnt++在机器指令层面上不是一条指令,而可能是从内存加载数据到寄存器、执行自增运算、保存寄存器中计算结果到内存这三部分,所以不进行保护的话有些更新是会丢失的。

    package main
    
    import (
        "fmt"
        "time"
        "sync/atomic"
        "runtime"
    )
    
    func main() {
        // IMPORTANT!!!
        runtime.GOMAXPROCS(4)
    
        // thread-unsafe
        var unsafeCnt int32 = 0
        for i := 0; i < 10; i++ {
            go func() {
                for i := 0; i < 20; i++ {
                    time.Sleep(time.Millisecond)
                    unsafeCnt++
                }
            }()
        }
        time.Sleep(time.Second)
        fmt.Println("cnt: ", unsafeCnt)
    
        // CAS toolkit
        var cnt int32 = 0
        for i := 0; i < 10; i++ {
            go func() {
                for i := 0; i < 20; i++ {
                    time.Sleep(time.Millisecond)
                    atomic.AddInt32(&cnt, 1)
                }
            }()
        }
    
        time.Sleep(time.Second)
        cntFinal := atomic.LoadInt32(&cnt)
        fmt.Println("cnt: ", cntFinal)
    }

    神奇CAS的原理
    Golang的AddInt32()类似于Java中AtomicInteger.incrementAndGet(),其伪代码可以表示如下。二者的基本思想是一致的,本质上是 乐观锁:首先,从内存位置M加载要修改的数据到寄存器A中;然后,修改数据并保存到另一寄存器B;最终,利用CPU提供的CAS指令(Java通过JNI调用到)用一条指令完成:1)A值与M处的原值比较;2)若相同则将B值覆盖到M处。
    若不相同,则CAS指令会失败,说明从内存加载到执行CAS指令这一小段时间内,发生了上下文切换,执行了其他线程的代码修改了M处的变量值。那么重新执行前面几个步骤再次尝试。
    ABA问题:即另一线程修改了M位置的数据,但是从原值改为C,又从C改回原值。这样上下文切换回来,CAS指令发现M处的值“未改变”(实际是改了两次,最后改回来了),所以CAS指令正常执行,不会失败。这种问题在Java中可以用AtomicStampedReference/AtomicMarkableReference解决。

    public final int incrementAndGet() {
        for (;;) {
            int current = get();
            int next = current + 1;
            if (compareAndSet(current, next))
                return next;
        }
    }

    4.3 Channel管道

    通过前面可以看到,尽管goroutine很方便很高效,但如果滥用的话很可能会导致并发安全问题。而Channel就是用来解决这个问题的,它是goroutine之间通信的桥梁,类似Actor模型中每个Actor的mailbox。多个goroutine要修改一个状态时,可以将请求都发送到一个Channel里,然后由一个goroutine负责顺序地修改状态。

    Channel默认是阻塞的,也就是说select时如果没有事件,那么当前goroutine会发生读阻塞。同理,Channel是有大小的,当Channel满了时,发送方会发生写阻塞。Channel这种阻塞的特性加上goroutine可以很容易就能实现生产者-消费者模式

    用case可以给Channel设置阻塞的超时时间,避免一直阻塞。而default则使select进入无阻塞模式

    package main
    
    import (
        "fmt"
        "time"
    )
    
    /**
     * Output:
     * received message: hello
     * received message: world
     * 
     * received from channel-1: Hello
     * received from channel-2: World
     * 
     * received message: hello
     * Time out!
     * 
     * Nothing received!
     * received message: hello
     * Nothing received!
     * Nothing received!
     * Nothing received!
     * Nothing received!
     * Nothing received!
     * Nothing received!
     * Nothing received!
     * Nothing received!
     * Nothing received!
     * received message: world
     * Nothing received!
     * Nothing received!
     * Nothing received!
     */
    func main() {
        listenOnChannel()
        selectTwoChannels()
    
        blockChannelWithTimeout()
        unblockChannel()
    }
    
    func listenOnChannel() {
        // Specify channel type and buffer size
        channel := make(chan string, 5)
    
        go func() {
            channel <- "hello"
            channel <- "world"
        }()
    
        for i := 0; i < 2; i++ {
            msg := <- channel
            fmt.Println("received message: " + msg)
        }
    }
    
    func selectTwoChannels() {
        c1 := make(chan string)
        c2 := make(chan string)
    
        go func() {
            time.Sleep(time.Second)
            c1 <- "Hello"
        }()
        go func() {
            time.Sleep(time.Second)
            c2 <- "World"
        }()
    
        for i := 0; i < 2; i++ {
            select {
                case msg1 := <- c1:
                    fmt.Println("received from channel-1: " + msg1)
                case msg2 := <- c2:
                    fmt.Println("received from channel-2: " + msg2)
            }
        }
    }
    
    func blockChannelWithTimeout() {
        channel := make(chan string, 5)
    
        go func() {
            channel <- "hello"
            // Sleep 10 sec
            time.Sleep(time.Second * 10)
            channel <- "world"
        }()
    
        for i := 0; i < 2; i++ {
            select {
                case msg := <- channel:
                    fmt.Println("received message: " + msg)
                // Set timeout 5 sec
                case <- time.After(time.Second * 5):
                    fmt.Println("Time out!")
            }
        }
    }
    
    func unblockChannel() {
        channel := make(chan string, 5)
    
        go func() {
            channel <- "hello"
            time.Sleep(time.Second * 10)
            channel <- "world"
        }()
    
        for i := 0; i < 15; i++ {
            select {
                case msg := <- channel:
                    fmt.Println("received message: " + msg)
                default:
                    fmt.Println("Nothing received!")
                    time.Sleep(time.Second)
            }
        }
    }

    4.4 缓冲流

    Golang的bufio包提供了方便的缓冲流操作,通过strings或网络IO得到流后,用bufio.NewReader/Writer()包装:

    • 缓冲区:Peek()或Read时,数据会从底层进入到缓冲区。缓冲区默认大小为4096字节。
    • 切片和拷贝:Peek()和ReadSlice()得到的都是切片(缓冲区数据的引用)而不是拷贝,所以更加节约空间。但是当缓冲区数据变化时,切片也会随之变化。而ReadBytes/String()得到的都是数据的拷贝,可以放心使用。
    • Unicode支持:ReadRune()可以直接读取Unicode字符。有意思的是Golang中Unicode字符也要用单引号,这点与Java不同。
    • 分隔符:ReadSlice/Bytes/String()得到的包含分隔符,bufio不会自动去掉。
    • Writer:对应地,Writer提供了WriteBytes/String/Rune。
    • undo方法:可以将读出的字节再放回到缓冲区,就像什么都没发生一样。
    package main
    
    import (
        "fmt"
        "strings"
        "bytes"
        "bufio"
    )
    
    /**
     * Buffered: 0
     * Buffered after peek: 7
     * ABCDE
     * AxCDE
     * 
     * abcdefghijklmnopqrst 20 <nil>
     * uvwxyz1234567890     16 <nil>
     *                      0  EOF
     *
     * "ABC "
     * "DEF "
     * "GHI"
     * 
     * "ABC "
     * "DEF "
     * "GHI"
     *
     * read unicode=[你], size=[3]
     * read unicode=[好], size=[3]
     * read(after undo) unicode=[好], size=[3]
     *
     * Available: 4096
     * Buffered: 0
     * Available after write: 4088
     * Buffered after write: 8
     * Buffer after write: ""
     * Available after flush: 4096
     * Buffered after flush: 0
     * Buffer after flush: "ABCDEFGH"
     *
     * Hello,世界!
     */
    func main() {
        testPeek()
        testRead()
        testReadSlice()
        testReadBytes()
        testReadUnicode()
    
        testWrite()
        testWriteByte()
    }
    
    func testPeek() {
        r := strings.NewReader("ABCDEFG")
        br := bufio.NewReader(r)
    
        fmt.Printf("Buffered: %d
    ", br.Buffered())
    
        p, _ := br.Peek(5)
        fmt.Printf("Buffered after peek: %d
    ", br.Buffered())
        fmt.Printf("%s
    ", p)
    
        p[1] = 'x'
        p, _ = br.Peek(5)
        fmt.Printf("%s
    ", p)
    }
    
    func testRead() {
        r := strings.NewReader("abcdefghijklmnopqrstuvwxyz1234567890")
        br := bufio.NewReader(r)
        b := make([]byte, 20)
    
        n, err := br.Read(b)
        fmt.Printf("%-20s %-2v %v
    ", b[:n], n, err)
    
        n, err = br.Read(b)
        fmt.Printf("%-20s %-2v %v
    ", b[:n], n, err)
    
        n, err = br.Read(b)
        fmt.Printf("%-20s %-2v %v
    ", b[:n], n, err)
    }
    
    func testReadSlice() {
        r := strings.NewReader("ABC DEF GHI")
        br := bufio.NewReader(r)
    
        w, _ := br.ReadSlice(' ')
        fmt.Printf("%q
    ", w)
    
        w, _ = br.ReadSlice(' ')
        fmt.Printf("%q
    ", w)
    
        w, _ = br.ReadSlice(' ')
        fmt.Printf("%q
    ", w)
    }
    
    func testReadBytes() {
        r := strings.NewReader("ABC DEF GHI")
        br := bufio.NewReader(r)
    
        w, _ := br.ReadBytes(' ')
        fmt.Printf("%q
    ", w)
    
        w, _ = br.ReadSlice(' ')
        fmt.Printf("%q
    ", w)
    
        s, _ := br.ReadString(' ')
        fmt.Printf("%q
    ", s)
    }
    
    func testReadUnicode() {
        r := strings.NewReader("你好,世界!")
        br := bufio.NewReader(r)
    
        c, size, _ := br.ReadRune()
        fmt.Printf("read unicode=[%c], size=[%v]
    ", c, size)
    
        c, size, _ = br.ReadRune()
        fmt.Printf("read unicode=[%c], size=[%v]
    ", c, size)
    
        br.UnreadRune()
        c, size, _ = br.ReadRune()
        fmt.Printf("read(after undo) unicode=[%c], size=[%v]
    ", c, size)
    }
    
    func testWrite() {
        b := bytes.NewBuffer(make([]byte, 0))
        bw := bufio.NewWriter(b)
    
        fmt.Printf("Available: %d
    ", bw.Available())
        fmt.Printf("Buffered: %d
    ", bw.Buffered())
    
        bw.WriteString("ABCDEFGH")
        fmt.Printf("Available after write: %d
    ", bw.Available())
        fmt.Printf("Buffered after write: %d
    ", bw.Buffered())
        fmt.Printf("Buffer after write: %q
    ", b)
    
        bw.Flush()
        fmt.Printf("Available after flush: %d
    ", bw.Available())
        fmt.Printf("Buffered after flush: %d
    ", bw.Buffered())
        fmt.Printf("Buffer after flush: %q
    ", b)
    }
    
    func testWriteByte() {
        b := bytes.NewBuffer(make([]byte, 0))
        bw := bufio.NewWriter(b)
    
        bw.WriteByte('H')
        bw.WriteByte('e')
        bw.WriteByte('l')
        bw.WriteByte('l')
        bw.WriteByte('o')
        bw.WriteString(",")
        bw.WriteRune('世')
        bw.WriteRune('界')
        bw.WriteRune('!')
        bw.Flush()
    
        fmt.Println(b)
    }

    4.5 并发控制

    sync包中的WaitGroup是个很有用的类,类似信号量。wg.Add()和Done()能够加减WaitGroup(信号量)的值,而Wait()会挂起当前线程直到信号量变为0。下面的例子用WaitGroup的值表示正在运行的goroutine数量。在goroutine中,用defer Done()确保goroutine正常或异常退出时,WaitGroup都能减一。

    package main
    
    import (
        "fmt"
        "sync"
    )
    
    /**
     * I'm waiting all goroutines on wg done
     * I'm done=[0]
     * I'm done=[1]
     * I'm done=[2]
     * I'm done=[3]
     * I'm done=[4]
     * I'm done=[5]
     * I'm done=[6]
     * I'm done=[7]
     * I'm done=[8]
     * I'm done=[9]
     */
    func main() {
        var wg sync.WaitGroup
        for i := 0; i < 10; i++ {
            wg.Add(1)
            go func(id int) {
                defer wg.Done()
                fmt.Printf("I'm done=[%d]
    ", id)
            }(i)
        }
    
        fmt.Println("I'm waiting all goroutines on wg done")
        wg.Wait()
    }

    4.6 网络编程

    Golang的net包的抽象层次还是挺高的,用不了几行代码就能实现一个简单的TCP或HTTP服务端了。

    4.6.1 Socket编程

    package main
    
    import (
        "net"
        "fmt"
        "io"
    )
    
    /**
     * Starting the server
     * Accept the connection:  127.0.0.1:14071
     * Warning: End of data EOF
     */
    func main() {
        listener, err := net.Listen("tcp", "127.0.0.1:12345")
        if err != nil {
            panic("error listen: " + err.Error())
        }
        fmt.Println("Starting the server")
    
        for {
            conn, err := listener.Accept()
            if err != nil {
                panic("error accept: " + err.Error())       
            }
            fmt.Println("Accept the connection: ", conn.RemoteAddr())
            go echoServer(conn)
        }
    }
    
    func echoServer(conn net.Conn) {
        buf := make([]byte, 1024)
        defer conn.Close()
    
        for {
            n, err := conn.Read(buf)
            switch err {
                case nil:
                    conn.Write(buf[0:n])
                case io.EOF:
                    fmt.Printf("Warning: End of data %s
    ", err)
                    return
                default:
                    fmt.Printf("Error: read data %s
    ", err)
                    return
            }
        }
    }

    4.6.2 Http服务器

    package main
    
    import (
        "fmt"
        "log"
        "net/http"
    )
    
    func main() {
        http.HandleFunc("/hello", handleHello)
        fmt.Println("serving on http://localhost:7777/hello")
        log.Fatal(http.ListenAndServe("localhost:7777", nil))
    }
    
    func handleHello(w http.ResponseWriter, req *http.Request) {
        log.Println("serving", req.URL)
        fmt.Fprintln(w, "Hello, world!")
    }

    5.结束语

    5.1 Golang初体验

    Golang的某些语法的确很简洁,像行尾无分号、条件语句无括号、类型推断、函数多返回值、异常处理、原生协程支持、DuckType继承等,尽管很多并不是Golang首创,但结合到一起写起来还是很舒服的。

    当然Golang也有让人“不爽”的地方。像变量和函数中的类型声明写在后面简直是“反人类”!同样是颠覆,switch的case默认会break就很实用。另外,因为Golang主要还是想替代C做系统开发,所以像类啊、包啊还是能看到C的影子,例如类声明只有成员变量而不会包含方法实现等,支持全局函数等,所以有时看到aaa.bbb()还是有点迷糊,不知道aaa是包名还是实例名。

    5.2 如何学习一门语言

    当我们谈到学习英语时,想到的可能是背单词、学语法、练习听说读写。对于编程语言来说,背单词(关键字)、学语法(语法规则)少不了,可听说读写只剩下了“写”,因为我们说话的对象是“冷冰冰”的计算机。所以唯一的捷径就是“写”,不断地练习!

    此外,学的语言多了也能总结出一些规律。首先是基础语法,包括了变量和常量、控制语句、函数、集合、OOP、异常处理、控制台输入输出、包管理等。然后是高级特性就差别比较大了。专注高并发的语言就要看并发方面的特性,专注OOP的语言就要看有哪些抽象层次更高的特性等等。还是那句话,基础语言只能说我们会用,而能够区别一门语言的高级特性才是它的根本和灵魂,也是我们要着重学习和领悟的地方。

  • 相关阅读:
    Redis主从同步原理-SYNC【转】
    redis3.0集群部署和测试
    Zabbix3.0配置邮件报警
    3分钟学会git命令的基础使用
    Rsync文件同步工具
    logstash grok 内置正则
    logrotate实现Mysql慢日志分割
    Python之unittest测试代码
    Zabbix如何实现批量监控端口状态
    Centos7搭建Confluence破解版
  • 原文地址:https://www.cnblogs.com/xiaomaohai/p/6157612.html
Copyright © 2020-2023  润新知