分类 软件开发 下的文章

在本文的 第一部分 的结尾,我承诺要写关于接口的内容。我不想在这里写有关接口或完整或简短的讲义。相反,我将展示一个简单的示例,来说明如何定义和使用接口,以及如何利用无处不在的 io.Writer 接口。还有一些关于 反射 reflection 半主机 semihosting 的内容。

STM32F030F4P6]

接口是 Go 语言的重要组成部分。如果你想了解更多有关它们的信息,我建议你阅读《高效的 Go 编程》 和 Russ Cox 的文章

并发 Blinky – 回顾

当你阅读前面示例的代码时,你可能会注意到一中打开或关闭 LED 的反直觉方式。 Set 方法用于关闭 LED,Clear 方法用于打开 LED。这是由于在 漏极开路配置 open-drain configuration 下驱动了 LED。我们可以做些什么来减少代码的混乱?让我们用 OnOff 方法来定义 LED 类型:

type LED struct {
    pin gpio.Pin
}

func (led LED) On() {
    led.pin.Clear()
}

func (led LED) Off() {
    led.pin.Set()
}

现在我们可以简单地调用 led.On()led.Off(),这不会再引起任何疑惑了。

在前面的所有示例中,我都尝试使用相同的 漏极开路配置 open-drain configuration 来避免代码复杂化。但是在最后一个示例中,对于我来说,将第三个 LED 连接到 GND 和 PA3 引脚之间并将 PA3 配置为 推挽模式 push-pull mode 会更容易。下一个示例将使用以此方式连接的 LED。

但是我们的新 LED 类型不支持推挽配置,实际上,我们应该将其称为 OpenDrainLED,并定义另一个类型 PushPullLED

type PushPullLED struct {
    pin gpio.Pin
}

func (led PushPullLED) On() {
    led.pin.Set()
}

func (led PushPullLED) Off() {
    led.pin.Clear()
}

请注意,这两种类型都具有相同的方法,它们的工作方式也相同。如果在 LED 上运行的代码可以同时使用这两种类型,而不必注意当前使用的是哪种类型,那就太好了。 接口类型可以提供帮助:

package main

import (
    "delay"

    "stm32/hal/gpio"
    "stm32/hal/system"
    "stm32/hal/system/timer/systick"
)

type LED interface {
    On()
    Off()
}

type PushPullLED struct{ pin gpio.Pin }

func (led PushPullLED) On()  {
    led.pin.Set()
}

func (led PushPullLED) Off() {
    led.pin.Clear()
}

func MakePushPullLED(pin gpio.Pin) PushPullLED {
    pin.Setup(&gpio.Config{Mode: gpio.Out, Driver: gpio.PushPull})
    return PushPullLED{pin}
}

type OpenDrainLED struct{ pin gpio.Pin }

func (led OpenDrainLED) On()  {
    led.pin.Clear()
}

func (led OpenDrainLED) Off() {
    led.pin.Set()
}

func MakeOpenDrainLED(pin gpio.Pin) OpenDrainLED {
    pin.Setup(&gpio.Config{Mode: gpio.Out, Driver: gpio.OpenDrain})
    return OpenDrainLED{pin}
}

var led1, led2 LED

func init() {
    system.SetupPLL(8, 1, 48/8)
    systick.Setup(2e6)

    gpio.A.EnableClock(false)
    led1 = MakeOpenDrainLED(gpio.A.Pin(4))
    led2 = MakePushPullLED(gpio.A.Pin(3))
}

func blinky(led LED, period int) {
    for {
        led.On()
        delay.Millisec(100)
        led.Off()
        delay.Millisec(period - 100)
    }
}

func main() {
    go blinky(led1, 500)
    blinky(led2, 1000)
}

我们定义了 LED 接口,它有两个方法: OnOffPushPullLEDOpenDrainLED 类型代表两种驱动 LED 的方式。我们还定义了两个用作构造函数的 Make*LED 函数。这两种类型都实现了 LED 接口,因此可以将这些类型的值赋给 LED 类型的变量:

led1 = MakeOpenDrainLED(gpio.A.Pin(4))
led2 = MakePushPullLED(gpio.A.Pin(3))

在这种情况下, 可赋值性 assignability 在编译时检查。赋值后,led1 变量包含一个 OpenDrainLED{gpio.A.Pin(4)},以及一个指向 OpenDrainLED 类型的方法集的指针。 led1.On() 调用大致对应于以下 C 代码:

led1.methods->On(led1.value)

如你所见,如果仅考虑函数调用的开销,这是相当廉价的抽象。

但是,对接口的任何赋值都会导致包含有关已赋值类型的大量信息。对于由许多其他类型组成的复杂类型,可能会有很多信息:

$ egc
$ arm-none-eabi-size cortexm0.elf
   text    data     bss     dec     hex filename
  10356     196     212   10764    2a0c cortexm0.elf

如果我们不使用 反射,可以通过避免包含类型和结构字段的名称来节省一些字节:

$ egc -nf -nt
$ arm-none-eabi-size cortexm0.elf
   text    data     bss     dec     hex filename
  10312     196     212   10720    29e0 cortexm0.elf

生成的二进制文件仍然包含一些有关类型的必要信息和关于所有导出方法(带有名称)的完整信息。在运行时,主要是当你将存储在接口变量中的一个值赋值给任何其他变量时,需要此信息来检查可赋值性。

我们还可以通过重新编译所导入的包来删除它们的类型和字段名称:

$ cd $HOME/emgo
$ ./clean.sh
$ cd $HOME/firstemgo
$ egc -nf -nt
$ arm-none-eabi-size cortexm0.elf
   text    data     bss     dec     hex filename
  10272     196     212   10680    29b8 cortexm0.elf

让我们加载这个程序,看看它是否按预期工作。这一次我们将使用 st-flash 命令:

$ arm-none-eabi-objcopy -O binary cortexm0.elf cortexm0.bin
$ st-flash write cortexm0.bin 0x8000000
st-flash 1.4.0-33-gd76e3c7
2018-04-10T22:04:34 INFO usb.c: -- exit_dfu_mode
2018-04-10T22:04:34 INFO common.c: Loading device parameters....
2018-04-10T22:04:34 INFO common.c: Device connected is: F0 small device, id 0x10006444
2018-04-10T22:04:34 INFO common.c: SRAM size: 0x1000 bytes (4 KiB), Flash: 0x4000 bytes (16 KiB) in pages of 1024 bytes
2018-04-10T22:04:34 INFO common.c: Attempting to write 10468 (0x28e4) bytes to stm32 address: 134217728 (0x8000000)
Flash page at addr: 0x08002800 erased
2018-04-10T22:04:34 INFO common.c: Finished erasing 11 pages of 1024 (0x400) bytes
2018-04-10T22:04:34 INFO common.c: Starting Flash write for VL/F0/F3/F1_XL core id
2018-04-10T22:04:34 INFO flash_loader.c: Successfully loaded flash loader in sram
 11/11 pages written
2018-04-10T22:04:35 INFO common.c: Starting verification of write complete
2018-04-10T22:04:35 INFO common.c: Flash written and verified! jolly good!

我没有将 NRST 信号连接到编程器,因此无法使用 -reset 选项,必须按下复位按钮才能运行程序。

Interfaces

看来,st-flash 与此板配合使用有点不可靠(通常需要复位 ST-LINK 加密狗)。此外,当前版本不会通过 SWD 发出复位命令(仅使用 NRST 信号)。软件复位是不现实的,但是它通常是有效的,缺少它会将会带来不便。对于 板卡程序员 board-programmer 来说 OpenOCD 工作得更好。

UART

UART( 通用异步收发传输器 Universal Aynchronous Receiver-Transmitter )仍然是当今微控制器最重要的外设之一。它的优点是以下属性的独特组合:

  • 相对较高的速度,
  • 仅两条信号线(在 半双工 half-duplex 通信的情况下甚至一条),
  • 角色对称,
  • 关于新数据的 同步带内信令 synchronous in-band signaling (起始位),
  • 在传输 words 内的精确计时。

这使得最初用于传输由 7-9 位的字组成的异步消息的 UART,也被用于有效地实现各种其他物理协议,例如被 WS28xx LEDs1-wire 设备使用的协议。

但是,我们将以其通常的角色使用 UART:从程序中打印文本消息。

package main

import (
    "io"
    "rtos"

    "stm32/hal/dma"
    "stm32/hal/gpio"
    "stm32/hal/irq"
    "stm32/hal/system"
    "stm32/hal/system/timer/systick"
    "stm32/hal/usart"
)

var tts *usart.Driver

func init() {
    system.SetupPLL(8, 1, 48/8)
    systick.Setup(2e6)

    gpio.A.EnableClock(true)
    tx := gpio.A.Pin(9)

    tx.Setup(&gpio.Config{Mode: gpio.Alt})
    tx.SetAltFunc(gpio.USART1_AF1)
    d := dma.DMA1
    d.EnableClock(true)
    tts = usart.NewDriver(usart.USART1, d.Channel(2, 0), nil, nil)
    tts.Periph().EnableClock(true)
    tts.Periph().SetBaudRate(115200)
    tts.Periph().Enable()
    tts.EnableTx()

    rtos.IRQ(irq.USART1).Enable()
    rtos.IRQ(irq.DMA1_Channel2_3).Enable()
}

func main() {
    io.WriteString(tts, "Hello, World!
")
}

func ttsISR() {
    tts.ISR()
}

func ttsDMAISR() {
    tts.TxDMAISR()
}

//c:__attribute__((section(".ISRs")))
var ISRs = [...]func(){
    irq.USART1:          ttsISR,
    irq.DMA1_Channel2_3: ttsDMAISR,
}

你会发现此代码可能有些复杂,但目前 STM32 HAL 中没有更简单的 UART 驱动程序(在某些情况下,简单的轮询驱动程序可能会很有用)。 usart.Driver 是使用 DMA 和中断来减轻 CPU 负担的高效驱动程序。

STM32 USART 外设提供传统的 UART 及其同步版本。要将其用作输出,我们必须将其 Tx 信号连接到正确的 GPIO 引脚:

tx.Setup(&gpio.Config{Mode: gpio.Alt})
tx.SetAltFunc(gpio.USART1_AF1)

在 Tx-only 模式下配置 usart.Driver (rxdma 和 rxbuf 设置为 nil):

tts = usart.NewDriver(usart.USART1, d.Channel(2, 0), nil, nil)

我们使用它的 WriteString 方法来打印这句名言。让我们清理所有内容并编译该程序:

$ cd $HOME/emgo
$ ./clean.sh
$ cd $HOME/firstemgo
$ egc
$ arm-none-eabi-size cortexm0.elf
  text       data        bss        dec        hex    filename
  12728        236        176      13140       3354    cortexm0.elf

要查看某些内容,你需要在 PC 中使用 UART 外设。

请勿使用 RS232 端口或 USB 转 RS232 转换器!

STM32 系列使用 3.3V 逻辑,但是 RS232 可以产生 -15 V ~ +15 V 的电压,这可能会损坏你的 MCU。你需要使用 3.3V 逻辑的 USB 转 UART 转换器。流行的转换器基于 FT232 或 CP2102 芯片。

UART

你还需要一些终端仿真程序(我更喜欢 picocom)。刷新新图像,运行终端仿真器,然后按几次复位按钮:

$ openocd -d0 -f interface/stlink.cfg -f target/stm32f0x.cfg -c 'init; program cortexm0.elf; reset run; exit'
Open On-Chip Debugger 0.10.0+dev-00319-g8f1f912a (2018-03-07-19:20)
Licensed under GNU GPL v2
For bug reports, read
        http://openocd.org/doc/doxygen/bugs.html
debug_level: 0
adapter speed: 1000 kHz
adapter_nsrst_delay: 100
none separate
adapter speed: 950 kHz
target halted due to debug-request, current mode: Thread
xPSR: 0xc1000000 pc: 0x080016f4 msp: 0x20000a20
adapter speed: 4000 kHz
** Programming Started **
auto erase enabled
target halted due to breakpoint, current mode: Thread
xPSR: 0x61000000 pc: 0x2000003a msp: 0x20000a20
wrote 13312 bytes from file cortexm0.elf in 1.020185s (12.743 KiB/s)
** Programming Finished **
adapter speed: 950 kHz
$
$ picocom -b 115200 /dev/ttyUSB0
picocom v3.1

port is        : /dev/ttyUSB0
flowcontrol    : none
baudrate is    : 115200
parity is      : none
databits are   : 8
stopbits are   : 1
escape is      : C-a
local echo is  : no
noinit is      : no
noreset is     : no
hangup is      : no
nolock is      : no
send_cmd is    : sz -vv
receive_cmd is : rz -vv -E
imap is        :
omap is        :
emap is        : crcrlf,delbs,
logfile is     : none
initstring     : none
exit_after is  : not set
exit is        : no

Type [C-a] [C-h] to see available commands
Terminal ready
Hello, World!
Hello, World!
Hello, World!

每次按下复位按钮都会产生新的 “Hello,World!”行。一切都在按预期进行。

要查看此 MCU 的 双向 bi-directional UART 代码,请查看 此示例

io.Writer 接口

io.Writer 接口可能是 Go 中第二种最常用的接口类型,仅次于 error 接口。其定义如下所示:

type Writer interface {
    Write(p []byte) (n int, err error)
}

usart.Driver 实现了 io.Writer,因此我们可以替换:

tts.WriteString("Hello, World!
")

io.WriteString(tts, "Hello, World!
")

此外,你需要将 io 包添加到 import 部分。

io.WriteString 函数的声明如下所示:

func WriteString(w Writer, s string) (n int, err error)

如你所见,io.WriteString 允许使用实现了 io.Writer 接口的任何类型来编写字符串。在内部,它检查基础类型是否具有 WriteString 方法,并使用该方法代替 Write(如果可用)。

让我们编译修改后的程序:

$ egc
$ arm-none-eabi-size cortexm0.elf
   text    data     bss     dec     hex filename
  15456     320     248   16024    3e98 cortexm0.elf

如你所见,io.WriteString 导致二进制文件的大小显着增加:15776-12964 = 2812 字节。 Flash 上没有太多空间了。是什么引起了这么大规模的增长?

使用这个命令:

arm-none-eabi-nm --print-size --size-sort --radix=d cortexm0.elf

我们可以打印两种情况下按其大小排序的所有符号。通过过滤和分析获得的数据(awkdiff),我们可以找到大约 80 个新符号。最大的十个如下所示:

> 00000062 T stm32$hal$usart$Driver$DisableRx
> 00000072 T stm32$hal$usart$Driver$RxDMAISR
> 00000076 T internal$Type$Implements
> 00000080 T stm32$hal$usart$Driver$EnableRx
> 00000084 t errors$New
> 00000096 R $8$stm32$hal$usart$Driver$$
> 00000100 T stm32$hal$usart$Error$Error
> 00000360 T io$WriteString
> 00000660 T stm32$hal$usart$Driver$Read

因此,即使我们不使用 usart.Driver.Read 方法,但它被编译进来了,与 DisableRxRxDMAISREnableRx 以及上面未提及的其他方法一样。不幸的是,如果你为接口赋值了一些内容,就需要它的完整方法集(包含所有依赖项)。对于使用大多数方法的大型程序来说,这不是问题。但是对于我们这种极简的情况而言,这是一个巨大的负担。

我们已经接近 MCU 的极限,但让我们尝试打印一些数字(你需要在 import 部分中用 strconv 替换 io 包):

func main() {
    a := 12
    b := -123

    tts.WriteString("a = ")
    strconv.WriteInt(tts, a, 10, 0, 0)
    tts.WriteString("
")
    tts.WriteString("b = ")
    strconv.WriteInt(tts, b, 10, 0, 0)
    tts.WriteString("
")

    tts.WriteString("hex(a) = ")
    strconv.WriteInt(tts, a, 16, 0, 0)
    tts.WriteString("
")
    tts.WriteString("hex(b) = ")
    strconv.WriteInt(tts, b, 16, 0, 0)
    tts.WriteString("
")
}

与使用 io.WriteString 函数的情况一样,strconv.WriteInt 的第一个参数的类型为 io.Writer

$ egc
/usr/local/arm/bin/arm-none-eabi-ld: /home/michal/firstemgo/cortexm0.elf section `.rodata' will not fit in region `Flash'
/usr/local/arm/bin/arm-none-eabi-ld: region `Flash' overflowed by 692 bytes
exit status 1

这一次我们的空间超出的不多。让我们试着精简一下有关类型的信息:

$ cd $HOME/emgo
$ ./clean.sh
$ cd $HOME/firstemgo
$ egc -nf -nt
$ arm-none-eabi-size cortexm0.elf
   text    data     bss     dec     hex filename
  15876     316     320   16512    4080 cortexm0.elf

很接近,但很合适。让我们加载并运行此代码:

a = 12
b = -123
hex(a) = c
hex(b) = -7b

Emgo 中的 strconv 包与 Go 中的原型有很大的不同。它旨在直接用于写入格式化的数字,并且在许多情况下可以替换沉重的 fmt 包。 这就是为什么函数名称以 Write 而不是 Format 开头,并具有额外的两个参数的原因。 以下是其用法示例:

func main() {
    b := -123
    strconv.WriteInt(tts, b, 10, 0, 0)
    tts.WriteString("
")
    strconv.WriteInt(tts, b, 10, 6, ' ')
    tts.WriteString("
")
    strconv.WriteInt(tts, b, 10, 6, '0')
    tts.WriteString("
")
    strconv.WriteInt(tts, b, 10, 6, '.')
    tts.WriteString("
")
    strconv.WriteInt(tts, b, 10, -6, ' ')
    tts.WriteString("
")
    strconv.WriteInt(tts, b, 10, -6, '0')
    tts.WriteString("
")
    strconv.WriteInt(tts, b, 10, -6, '.')
    tts.WriteString("
")
}

下面是它的输出:

-123
  -123
-00123
..-123
-123
-123
-123..

Unix 流 和 莫尔斯电码 Morse code

由于大多数写入的函数都使用 io.Writer 而不是具体类型(例如 C 中的 FILE ),因此我们获得了类似于 Unix stream 的功能。在 Unix 中,我们可以轻松地组合简单的命令来执行更大的任务。例如,我们可以通过以下方式将文本写入文件:

echo "Hello, World!" > file.txt

> 操作符将前面命令的输出流写入文件。还有 | 操作符,用于连接相邻命令的输出流和输入流。

多亏了流,我们可以轻松地转换/过滤任何命令的输出。例如,要将所有字母转换为大写,我们可以通过 tr 命令过滤 echo 的输出:

echo "Hello, World!" | tr a-z A-Z > file.txt

为了显示 io.Writer 和 Unix 流之间的类比,让我们编写以下代码:

io.WriteString(tts, "Hello, World!
")

采用以下伪 unix 形式:

io.WriteString "Hello, World!" | usart.Driver usart.USART1

下一个示例将显示如何执行此操作:

io.WriteString "Hello, World!" | MorseWriter | usart.Driver usart.USART1

让我们来创建一个简单的编码器,它使用莫尔斯电码对写入的文本进行编码:

type MorseWriter struct {
    W io.Writer
}

func (w *MorseWriter) Write(s []byte) (int, error) {
    var buf [8]byte
    for n, c := range s {
        switch {
        case c == '\n':
            c = ' ' // Replace new lines with spaces.
        case 'a' <= c && c <= 'z':
            c -= 'a' - 'A' // Convert to upper case.
        }
        if c < ' ' || 'Z' < c {
            continue // c is outside ASCII [' ', 'Z']
        }
        var symbol morseSymbol
        if c == ' ' {
            symbol.length = 1
            buf[0] = ' '
        } else {
            symbol = morseSymbols[c-'!']
            for i := uint(0); i < uint(symbol.length); i++ {
                if (symbol.code>>i)&1 != 0 {
                    buf[i] = '-'
                } else {
                    buf[i] = '.'
                }
            }
        }
        buf[symbol.length] = ' '
        if _, err := w.W.Write(buf[:symbol.length+1]); err != nil {
            return n, err
        }
    }
    return len(s), nil
}

type morseSymbol struct {
    code, length byte
}

//emgo:const
var morseSymbols = [...]morseSymbol{
    {1<<0 | 1<<1 | 1<<2, 4}, // ! ---.
    {1<<1 | 1<<4, 6},        // " .-..-.
    {},                      // #
    {1<<3 | 1<<6, 7},        // $ ...-..-

    // Some code omitted...

    {1<<0 | 1<<3, 4},        // X -..-
    {1<<0 | 1<<2 | 1<<3, 4}, // Y -.--
    {1<<0 | 1<<1, 4},        // Z --..
}

你可以在 这里 找到完整的 morseSymbols 数组。 //emgo:const 指令确保 morseSymbols 数组不会被复制到 RAM 中。

现在我们可以通过两种方式打印句子:

func main() {
    s := "Hello, World!
"
    mw := &MorseWriter{tts}

    io.WriteString(tts, s)
    io.WriteString(mw, s)
}

我们使用指向 MorseWriter &MorseWriter{tts} 的指针而不是简单的 MorseWriter{tts} 值,因为 MorseWriter 太大,不适合接口变量。

与 Go 不同,Emgo 不会为存储在接口变量中的值动态分配内存。接口类型的大小受限制,相当于三个指针(适合 slice )或两个 float64(适合 complex128)的大小,以较大者为准。它可以直接存储所有基本类型和小型 “结构体/数组” 的值,但是对于较大的值,你必须使用指针。

让我们编译此代码并查看其输出:

$ egc
$ arm-none-eabi-size cortexm0.elf
   text    data     bss     dec     hex filename
  15152     324     248   15724    3d6c cortexm0.elf
Hello, World!
.... . .-.. .-.. --- --..--   .-- --- .-. .-.. -.. ---.

终极闪烁

Blinky 是等效于 “Hello,World!” 程序的硬件。一旦有了摩尔斯编码器,我们就可以轻松地将两者结合起来以获得终极闪烁程序:

package main

import (
    "delay"
    "io"

    "stm32/hal/gpio"
    "stm32/hal/system"
    "stm32/hal/system/timer/systick"
)

var led gpio.Pin

func init() {
    system.SetupPLL(8, 1, 48/8)
    systick.Setup(2e6)

    gpio.A.EnableClock(false)
    led = gpio.A.Pin(4)

    cfg := gpio.Config{Mode: gpio.Out, Driver: gpio.OpenDrain, Speed: gpio.Low}
    led.Setup(&cfg)
}

type Telegraph struct {
    Pin   gpio.Pin
    Dotms int // Dot length [ms]
}

func (t Telegraph) Write(s []byte) (int, error) {
    for _, c := range s {
        switch c {
        case '.':
            t.Pin.Clear()
            delay.Millisec(t.Dotms)
            t.Pin.Set()
            delay.Millisec(t.Dotms)
        case '-':
            t.Pin.Clear()
            delay.Millisec(3 * t.Dotms)
            t.Pin.Set()
            delay.Millisec(t.Dotms)
        case ' ':
            delay.Millisec(3 * t.Dotms)
        }
    }
    return len(s), nil
}

func main() {
    telegraph := &MorseWriter{Telegraph{led, 100}}
    for {
        io.WriteString(telegraph, "Hello, World! ")
    }
}

// Some code omitted...

在上面的示例中,我省略了 MorseWriter 类型的定义,因为它已在前面展示过。完整版可通过 这里 获取。让我们编译它并运行:

$ egc
$ arm-none-eabi-size cortexm0.elf
   text    data     bss     dec     hex filename
  11772     244     244   12260    2fe4 cortexm0.elf

Ultimate Blinky

反射

是的,Emgo 支持 反射reflect 包尚未完成,但是已完成的部分足以实现 fmt.Print 函数族了。来看看我们可以在小型 MCU 上做什么。

为了减少内存使用,我们将使用 半主机 semihosting 作为标准输出。为了方便起见,我们还编写了简单的 println 函数,它在某种程度上类似于 fmt.Println

package main

import (
    "debug/semihosting"
    "reflect"
    "strconv"

    "stm32/hal/system"
    "stm32/hal/system/timer/systick"
)

var stdout semihosting.File

func init() {
    system.SetupPLL(8, 1, 48/8)
    systick.Setup(2e6)

    var err error
    stdout, err = semihosting.OpenFile(":tt", semihosting.W)
    for err != nil {
    }
}

type stringer interface {
    String() string
}

func println(args ...interface{}) {
    for i, a := range args {
        if i > 0 {
            stdout.WriteString(" ")
        }
        switch v := a.(type) {
        case string:
            stdout.WriteString(v)
        case int:
            strconv.WriteInt(stdout, v, 10, 0, 0)
        case bool:
            strconv.WriteBool(stdout, v, 't', 0, 0)
        case stringer:
            stdout.WriteString(v.String())
        default:
            stdout.WriteString("%unknown")
        }
    }
    stdout.WriteString("
")
}

type S struct {
    A int
    B bool
}

func main() {
    p := &S{-123, true}

    v := reflect.ValueOf(p)

    println("kind(p) =", v.Kind())
    println("kind(*p) =", v.Elem().Kind())
    println("type(*p) =", v.Elem().Type())

    v = v.Elem()

    println("*p = {")
    for i := 0; i < v.NumField(); i++ {
        ft := v.Type().Field(i)
        fv := v.Field(i)
        println("  ", ft.Name(), ":", fv.Interface())
    }
    println("}")
}

semihosting.OpenFile 函数允许在主机端打开/创建文件。特殊路径 :tt 对应于主机的标准输出。

println 函数接受任意数量的参数,每个参数的类型都是任意的:

func println(args ...interface{})

可能是因为任何类型都实现了空接口 interface{}println 使用 类型开关 打印字符串,整数和布尔值:

switch v := a.(type) {
case string:
    stdout.WriteString(v)
case int:
    strconv.WriteInt(stdout, v, 10, 0, 0)
case bool:
    strconv.WriteBool(stdout, v, 't', 0, 0)
case stringer:
    stdout.WriteString(v.String())
default:
    stdout.WriteString("%unknown")
}

此外,它还支持任何实现了 stringer 接口的类型,即任何具有 String() 方法的类型。在任何 case 子句中,v 变量具有正确的类型,与 case 关键字后列出的类型相同。

reflect.ValueOf(p) 函数通过允许以编程的方式分析其类型和内容的形式返回 p。如你所见,我们甚至可以使用 v.Elem() 取消引用指针,并打印所有结构体及其名称。

让我们尝试编译这段代码。现在让我们看看如果编译时没有类型和字段名,会有什么结果:

$ egc -nt -nf
$ arm-none-eabi-size cortexm0.elf
   text    data     bss     dec     hex filename
  16028     216     312   16556    40ac cortexm0.elf

闪存上只剩下 140 个可用字节。让我们使用启用了半主机的 OpenOCD 加载它:

$ openocd -d0 -f interface/stlink.cfg -f target/stm32f0x.cfg -c 'init; program cortexm0.elf; arm semihosting enable; reset run'
Open On-Chip Debugger 0.10.0+dev-00319-g8f1f912a (2018-03-07-19:20)
Licensed under GNU GPL v2
For bug reports, read
        http://openocd.org/doc/doxygen/bugs.html
debug_level: 0
adapter speed: 1000 kHz
adapter_nsrst_delay: 100
none separate
adapter speed: 950 kHz
target halted due to debug-request, current mode: Thread
xPSR: 0xc1000000 pc: 0x08002338 msp: 0x20000a20
adapter speed: 4000 kHz
** Programming Started **
auto erase enabled
target halted due to breakpoint, current mode: Thread
xPSR: 0x61000000 pc: 0x2000003a msp: 0x20000a20
wrote 16384 bytes from file cortexm0.elf in 0.700133s (22.853 KiB/s)
** Programming Finished **
semihosting is enabled
adapter speed: 950 kHz
kind(p) = ptr
kind(*p) = struct
type(*p) =
*p = {
   X. : -123
   X. : true
}

如果你实际运行此代码,则会注意到半主机运行缓慢,尤其是在逐字节写入时(缓冲很有用)。

如你所见,*p 没有类型名称,并且所有结构字段都具有相同的 X. 名称。让我们再次编译该程序,这次不带 -nt -nf 选项:

$ egc
$ arm-none-eabi-size cortexm0.elf
   text    data     bss     dec     hex filename
  16052     216     312   16580    40c4 cortexm0.elf

现在已经包括了类型和字段名称,但仅在 main.go 文件中 main 包中定义了它们。该程序的输出如下所示:

kind(p) = ptr
kind(*p) = struct
type(*p) = S
*p = {
   A : -123
   B : true
}

反射是任何易于使用的序列化库的关键部分,而像 JSON 这样的序列化 算法 物联网 IoT 时代也越来越重要。

这些就是我完成的本文的第二部分。我认为有机会进行第三部分,更具娱乐性的部分,在那里我们将各种有趣的设备连接到这块板上。如果这块板装不下,我们就换一块大一点的。


via: https://ziutek.github.io/2018/04/14/go_on_very_small_hardware2.html

作者:Michał Derkacz 译者:gxlct008 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

本文是该系列的第九篇,也是最后一篇。

在这篇文章中,我们将对 对话 conversation 页面进行编码。此页面是两个用户之间的聊天室。在顶部我们将显示其他参与者的信息,下面接着的是最新消息列表,以及底部的消息表单。

聊天标题

让我们从创建 static/pages/conversation-page.js 文件开始,它包含以下内容:

import http from '../http.js'
import { navigate } from '../router.js'
import { avatar, escapeHTML } from '../shared.js'

export default async function conversationPage(conversationID) {
    let conversation
    try {
        conversation = await getConversation(conversationID)
    } catch (err) {
        alert(err.message)
        navigate('/', true)
        return
    }

    const template = document.createElement('template')
    template.innerHTML = `
 <div>
 <a href="/">← Back</a>
 ${avatar(conversation.otherParticipant)}
 <span>${conversation.otherParticipant.username}</span>
 </div>
 <!-- message list here -->
 <!-- message form here -->
 `
    const page = template.content
    return page
}

function getConversation(id) {
    return http.get('/api/conversations/' + id)
}

此页面接收路由从 URL 中提取的会话 ID。

首先,它向 /api/ conversations/{conversationID} 发起一个 GET 请求,以获取有关对话的信息。 如果出现错误,我们会将其显示,并重定向回 /。然后我们呈现有关其他参与者的信息。

对话列表

我们也会获取最新的消息并显示它们。

let conversation, messages
try {
    [conversation, messages] = await Promise.all([
        getConversation(conversationID),
        getMessages(conversationID),
    ])
}

更新 conversationPage() 函数以获取消息。我们使用 Promise.all() 同时执行这两个请求。

function getMessages(conversationID) {
    return http.get(`/api/conversations/${conversationID}/messages`)
}

发起对 /api/conversations/{conversationID}/messages 的 GET 请求可以获取对话中的最新消息。

<ol id="messages"></ol>

现在,将该列表添加到标记中。

const messagesOList = page.getElementById('messages')
for (const message of messages.reverse()) {
    messagesOList.appendChild(renderMessage(message))
}

这样我们就可以将消息附加到列表中了。我们以时间倒序来显示它们。

function renderMessage(message) {
    const messageContent = escapeHTML(message.content)
    const messageDate = new Date(message.createdAt).toLocaleString()

    const li = document.createElement('li')
    if (message.mine) {
        li.classList.add('owned')
    }
    li.innerHTML = `
 <p>${messageContent}</p>
 <time>${messageDate}</time>
 `
    return li
}

每个消息条目显示消息内容本身及其时间戳。使用 .mine,我们可以将不同的 css 类附加到条目,这样您就可以将消息显示在右侧。

消息表单

<form id="message-form">
    <input type="text" placeholder="Type something" maxlength="480" required>
    <button>Send</button>
</form>

将该表单添加到当前标记中。

page.getElementById('message-form').onsubmit = messageSubmitter(conversationID)

将事件监听器附加到 “submit” 事件。

function messageSubmitter(conversationID) {
    return async ev => {
        ev.preventDefault()

        const form = ev.currentTarget
        const input = form.querySelector('input')
        const submitButton = form.querySelector('button')

        input.disabled = true
        submitButton.disabled = true

        try {
            const message = await createMessage(input.value, conversationID)
            input.value = ''
            const messagesOList = document.getElementById('messages')
            if (messagesOList === null) {
                return
            }

            messagesOList.appendChild(renderMessage(message))
        } catch (err) {
            if (err.statusCode === 422) {
                input.setCustomValidity(err.body.errors.content)
            } else {
                alert(err.message)
            }
        } finally {
            input.disabled = false
            submitButton.disabled = false

            setTimeout(() => {
                input.focus()
            }, 0)
        }
    }
}

function createMessage(content, conversationID) {
    return http.post(`/api/conversations/${conversationID}/messages`, { content })
}

我们利用 partial application 在 “submit” 事件处理程序中获取对话 ID。它 从输入中获取消息内容,并用它对 /api/conversations/{conversationID}/messages 发出 POST 请求。 然后将新创建的消息添加到列表中。

消息订阅

为了实现实时,我们还将订阅此页面中的消息流。

page.addEventListener('disconnect', subscribeToMessages(messageArriver(conversationID)))

将该行添加到 conversationPage() 函数中。

function subscribeToMessages(cb) {
    return http.subscribe('/api/messages', cb)
}

function messageArriver(conversationID) {
    return message => {
        if (message.conversationID !== conversationID) {
            return
        }

        const messagesOList = document.getElementById('messages')
        if (messagesOList === null) {
            return

        }
        messagesOList.appendChild(renderMessage(message))
        readMessages(message.conversationID)
    }
}

function readMessages(conversationID) {
    return http.post(`/api/conversations/${conversationID}/read\_messages`)
}

在这里我们仍然使用这个应用的部分来获取会话 ID。 当新消息到达时,我们首先检查它是否来自此对话。如果是,我们会将消息条目预先添加到列表中,并向 /api/conversations/{conversationID}/read_messages 发起 POST 一个请求,以更新参与者上次阅读消息的时间。


本系列到此结束。 消息应用现在可以运行了。


via: https://nicolasparada.netlify.com/posts/go-messenger-conversation-page/

作者:Nicolás Parada 选题:lujun9972 译者:gxlct008 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

我们将所有的 C 语言要素放置到一份易读的备忘录上。

1972 年, 丹尼斯·里奇 Dennis Ritchie 任职于 贝尔实验室 Bell Labs ,在几年前,他和他的团队成员发明了 Unix 。在创建了一个经久不衰的操作系统(至今仍在使用)之后,他需要一种好的方法来对这些 Unix 计算机编程,以便它们可用执行新的任务。在现在看来,这很奇怪,但在当时,编程语言相对较少,Fortran、Lisp、Algol 以及 B 语言都很流行,但是,对于贝尔实验室的研究员们想要做的事情来说,它们还是远远不够的。丹尼斯·里奇表现出一种后来被称为程序员的主要特征的特质:创造了他自己的解决方案。他称之为 C 语言,并且在近 50 年后,它仍在广泛的使用。

为什么你应该学习 C 语言

今天,有很多语言为程序员提供了比 C 语言更多的特性。最明显的是 C++ 语言,这是一种以相当露骨的方式命名的语言,它构建在 C 语言之上,创建了一种很好的面向对象语言。不过,许多其它语言的存在是有充分理由的。计算机擅长一致的重复,因此任何可预见的东西都可以构建在编程语言中,对程序员来说这意味着更少的工作量。为什么在 C++ 语言中用一行语句就可以将一个 int 转换为一个 long 时(long x = long(n);),还要在 C 语言用两行语句呢?

然而,C 语言在今天仍然有用。

首先,C 语言是一种相当简约和直接的语言。除了编程的基础知识之外,并没有很高级的概念,这很大程度上是因为 C 语言实际上就是现代编程语言的基础之一。例如,C 语言的特性之一是数组,但是它不提供字典(除非你自己写一个)。当你学习 C 语言时,你会学习编程的基础组成部分,它可以帮助你认识到如今的编程语言的改进及其的精心设计。

因为 C 语言是一种最小化的编程语言,你的应用程序很可能会获得性能上的提升,这在其它许多编程语言中是看不到的。当你考虑你的代码可以执行多快的时候,很容易陷入锱铢必较的境地,因此,重要的是要问清楚你是否需要为某一特定任务提供更多的速度。与 Python 或 Java 相比,使用 C 语言,你在每行代码中需要纠结的地方更少。C 语言程序运行很快。这是 Linux 内核使用 C 语言编写的一个很好的理由。

最后,C 语言很容易入门,特别是,如果你正在运行 Linux,就已经能运行 C 语言代码了,因为 Linux 系统包含 GNU C 库(glibc)。为了编写和构建 C 语言程序,你需要做的全部工作就是安装一个编译器,打开一个文本编辑器,开始编码。

开始学习 C 语言

如果你正在运行 Linux ,你可以使用你的软件包管理器安装一个 C 编译器。在 Fedora 或 RHEL 上:

$ sudo dnf install gcc

在 Debian 及其衍生系统上:

$ sudo apt install build-essential

在 macOS 上,你可以 安装 Homebrew ,并使用它来安装 GCC

$ brew install gcc

在 Windows 上, 你可以使用 MinGW 安装一套最小的包含 GCC 的 GNU 实用程序集。

在 Linux 或 macOS 上验证你已经安装的 GCC:

$ gcc --version
gcc (GCC) x.y.z
Copyright (C) 20XX Free Software Foundation, Inc.

在 Windows 上,提供 EXE 文件的完整路径:

PS> C:\MinGW\bin\gcc.exe --version
gcc.exe (MinGW.org GCC Build-2) x.y.z
Copyright (C) 20XX Free Software Foundation, Inc.

C 语法

C 语言不是一种脚本语言。它是一种编译型语言,这意味着它由 C 编译器处理来产生一个二进制可执行文件。这不同于脚本语言(如 Bash)或混合型语言(如 Python)。

在 C 语言中,你可以创建函数来执行你希望做到的任务。默认情况下,执行的是一个名为 main 的函数。

这里是一个使用 C 语言写的简单的 “hello world” 程序:

#include <stdio.h>

int main() {
  printf("Hello world");
  return 0;
}

第一行包含一个被称为 stdio.h(标准输入和输出)的 头文件,它基本上是自由使用的、非常初级的 C 语言代码,你可以在你自己的程序中重复使用它。然后创建了一个由一条基本的输出语句构成的名为 main 的函数。保存这些文本到一个被称为 hello.c 的文件中,然后使用 GCC 编译它:

$ gcc hello.c --output hello

尝试运行你的 C 语言程序:

$ ./hello
Hello world$

返回值

这是 Unix 哲学的一部分,一个函数在执行后“返回”一些东西:在成功时不返回任何东西,在失败时返回其它的一些东西(例如,一个错误信息)。这些返回的内容通常使用数字(确切地说是整数)表示:0 表示没有错误,任何大于 0 的数字都表示一些不成功的状态。

Unix 和 Linux 被设计成在运行成功时保持沉默是很明智的。这是为了让你在执行一系列命令时,假设没有任何错误或警告会妨碍你的工作,从而可以始终为成功执行做准备。类似地,在 C 语言中的函数在设计上也预期不出现错误。

你可以通过一个小的修改,让你的程序看起来是失败的,就可以看到这一点:

include <stdio.h>

int main() {
  printf("Hello world");
  return 1;
}

编译它:

$ gcc hello.c --output failer

现在使用一个内置的 Linux 测试方式来运行它。仅在成功时,&& 操作符才会执行一个命令的第二部分。例如:

$ echo "success" && echo "it worked"
success
it worked

失败时,|| 测试会执行一个命令的第二部分。

$ ls blah || echo "it did not work"
ls: cannot access 'blah': No such file or directory
it did not work

现在,尝试你的程序,在成功时,它返回 0;而是返回 1

$ ./failer && echo "it worked"
String is: hello

这个程序成功地执行了,但是没有触发第二个命令。

变量和类型

在一些语言中,你可以创建变量而不具体指定变量所包含的数据的类型。这些语言如此设计使得解释器需要对一个变量运行一些测试来视图发现变量是什么样的数据类型。例如,var=1 定义了一个整型数,当你创建一个表达式将 var 与某些东西相加时,Python 知道显然它是一个整型数。它同样知道当你连接 helloworld 时,单词 world 是一个字符串。

C 语言不会为你做任何这些识别和调查;你必须自己定义你的变量类型。这里有几种变量类型,包括整型(int),字符型(char),浮点型(float),布尔型(boolean)。

你可能也注意到这里没有字符串类型。与 Python 和 Java 和 Lua 以及其它的编程语言不同,C 语言没有字符串类型,而是将字符串看作一个字符数组。

这里是一些简单的代码,它建立了一个 char 数组变量,然后使用 printf 将数组变量和一段简单的信息打印到你的屏幕上:

#include <stdio.h>

int main() {
   char var[6] = "hello";
   printf("Your string is: %s
",var);
}

你可能会注意到,这个代码示例向一个由五个字母组成的单词提供了六个字符的空间。这是因为在字符串的结尾有处一个隐藏的终止符,它占用了数组中的一个字节。你可以通过编译和执行代码来运行它:

$ gcc hello.c --output hello
$ ./hello
hello

函数

和其它的编程语言一样,C 函数也接受可选的参数。你可以通过定义你希望函数接受的数据类型,来将参数从一个函数传递到另一个函数:

#include <stdio.h>

int printmsg(char a[]) {
   printf("String is: %s
",a);
}

int main() {
   char a[6] = "hello";
   printmsg(a);
   return 0;
}

简单地将一个函数分解为两个函数的这种方法并不是非常有用,但是它演示了默认运行 main 函数以及如何在函数之间传递数据。

条件语句

在真实的编程中,你通常希望你的代码根据数据做出判断。这是使用条件语句完成的,if 语句是其中最基础的一个语句。

为了使这个示例程序更具动态性,你可以包含 string.h 头文件,顾名思义,它包含用于检查字符串的代码。尝试使用来自 string.h 文件中的 strlen 函数测试传递给 printmsg 函数的字符串是否大于 0

#include <stdio.h>
#include <string.h>

int printmsg(char a[]) {
  size_t len = strlen(a);
  if ( len > 0) {
    printf("String is: %s
",a);
  }
}

int main() {
   char a[6] = "hello";
   printmsg(a);
   return 1;
}

正如在这个示例中所实现的,该条件永远都不会是非真的,因为所提供的字符串总是 hello,它的长度总是大于 0。这个不够认真的重新实现的 echo 命令的最后一点要做是接受来自用户的输入。

命令参数

stdio.h 文件包含的代码在每次程序启动时提供了两个参数: 一个是命令中包含多少项的计数(argc),一个是包含每个项的数组(argv)。例如, 假设你发出这个虚构的命令:

$ foo -i bar

argc3argv 的内容是:

  • argv[0] = foo
  • argv[1] = -i
  • argv[2] = bar

你可以修改示例 C 语言程序来以字符串方式接受 argv[2],而不是默认的 hello 吗?

命令式编程语言

C 语言是一种命令式编程语言。它不是面向对象的,也没有类结构。使用 C 语言的经验可以教你很多关于如何处理数据,以及如何更好地管理你的代码运行时生成的数据。多使用 C 语言,你最后能够编写出其它语言(例如 Python 和 Lua)可以使用的库。

想要了解更多关于 C 的知识,你需要使用它。在 /usr/include/ 中查找有用的 C 语言头文件,并且看看你可以做什么小任务来使 C 语言对你有用。在学习的过程中,使用来自 FreeDOS 的 Jim Hall 编写的 C 语言忘备录。它在一张双面纸忘备录上放置了所有的基本要素,所以在你练习时,可以立即访问 C 语言语法的所有要素。


via: https://opensource.com/article/20/8/c-programming-cheat-sheet

作者:Seth Kenlon 选题:lujun9972 译者:robsean 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

本文是该系列的第八篇。

继续前端部分,让我们在本文中完成 home 页面的开发。 我们将添加一个开始对话的表单和一个包含最新对话的列表。

对话表单

转到 static/ages/home-page.js 文件,在 HTML 视图中添加一些标记。

<form id="conversation-form">
    <input type="search" placeholder="Start conversation with..." required>
</form>

将该表单添加到我们显示 “auth user” 和 “logout” 按钮部分的下方。

page.getElementById('conversation-form').onsubmit = onConversationSubmit

现在我们可以监听 “submit” 事件来创建对话了。

import http from '../http.js'
import { navigate } from '../router.js'

async function onConversationSubmit(ev) {
    ev.preventDefault()

    const form = ev.currentTarget
    const input = form.querySelector('input')

    input.disabled = true

    try {
        const conversation = await createConversation(input.value)
        input.value = ''
        navigate('/conversations/' + conversation.id)
    } catch (err) {
        if (err.statusCode === 422) {
            input.setCustomValidity(err.body.errors.username)
        } else {
            alert(err.message)
        }
        setTimeout(() => {
            input.focus()
        }, 0)
    } finally {
        input.disabled = false
    }
}

function createConversation(username) {
    return http.post('/api/conversations', { username })
}

在提交时,我们使用用户名对 /api/conversations 进行 POST 请求,并重定向到 conversation 页面(用于下一篇文章)。

对话列表

还是在这个文件中,我们将创建 homePage() 函数用来先异步加载对话。

export default async function homePage() {
    const conversations = await getConversations().catch(err => {
        console.error(err)
        return []
    })
    /\*...\*/
}

function getConversations() {
    return http.get('/api/conversations')
}

然后,在标记中添加一个列表来渲染对话。

<ol id="conversations"></ol>

将其添加到当前标记的正下方。

const conversationsOList = page.getElementById('conversations')
for (const conversation of conversations) {
    conversationsOList.appendChild(renderConversation(conversation))
}

因此,我们可以将每个对话添加到这个列表中。

import { avatar, escapeHTML } from '../shared.js'

function renderConversation(conversation) {
    const messageContent = escapeHTML(conversation.lastMessage.content)
    const messageDate = new Date(conversation.lastMessage.createdAt).toLocaleString()

    const li = document.createElement('li')
    li.dataset['id'] = conversation.id
    if (conversation.hasUnreadMessages) {
        li.classList.add('has-unread-messages')
    }
    li.innerHTML = `
 <a href="/conversations/${conversation.id}">
 <div>
 ${avatar(conversation.otherParticipant)}
 <span>${conversation.otherParticipant.username}</span>
 </div>
 <div>
 <p>${messageContent}</p>
 <time>${messageDate}</time>
 </div>
 </a>
 `
    return li
}

每个对话条目都包含一个指向对话页面的链接,并显示其他参与者信息和最后一条消息的预览。另外,您可以使用 .hasUnreadMessages 向该条目添加一个类,并使用 CSS 进行一些样式设置。也许是粗体字体或强调颜色。

请注意,我们需要转义信息的内容。该函数来自于 static/shared.js 文件:

export function escapeHTML(str) {
    return str
        .replace(/&/g, '&amp;')
        .replace(/</g, '&lt;')
        .replace(/>/g, '&gt;')
        .replace(/"/g, '&quot;')
        .replace(/'/g, '&#039;')
}

这会阻止将用户编写的消息显示为 HTML。如果用户碰巧编写了类似以下内容的代码:

<script>alert('lololo')</script>

这将非常烦人,因为该脚本将被执行?。所以,永远记住要转义来自不可信来源的内容。

消息订阅

最后但并非最不重要的一点,我想在这里订阅消息流。

const unsubscribe = subscribeToMessages(onMessageArrive)
page.addEventListener('disconnect', unsubscribe)

homePage() 函数中添加这一行。

function subscribeToMessages(cb) {
    return http.subscribe('/api/messages', cb)
}

函数 subscribe() 返回一个函数,该函数一旦调用就会关闭底层连接。这就是为什么我把它传递给 “断开连接” disconnect 事件的原因;因此,当用户离开页面时,事件流将被关闭。

async function onMessageArrive(message) {
    const conversationLI = document.querySelector(`li[data-id="${message.conversationID}"]`)
    if (conversationLI !== null) {
        conversationLI.classList.add('has-unread-messages')
        conversationLI.querySelector('a > div > p').textContent = message.content
        conversationLI.querySelector('a > div > time').textContent = new Date(message.createdAt).toLocaleString()
        return
    }

    let conversation
    try {
        conversation = await getConversation(message.conversationID)
        conversation.lastMessage = message
    } catch (err) {
        console.error(err)
        return
    }

    const conversationsOList = document.getElementById('conversations')
    if (conversationsOList === null) {
        return
    }

    conversationsOList.insertAdjacentElement('afterbegin', renderConversation(conversation))
}

function getConversation(id) {
    return http.get('/api/conversations/' + id)
}

每次有新消息到达时,我们都会在 DOM 中查询会话条目。如果找到,我们会将 has-unread-messages 类添加到该条目中,并更新视图。如果未找到,则表示该消息来自刚刚创建的新对话。我们去做一个对 /api/conversations/{conversationID} 的 GET 请求,以获取在其中创建消息的对话,并将其放在对话列表的前面。


以上这些涵盖了主页的所有内容 ?。 在下一篇文章中,我们将对 conversation 页面进行编码。


via: https://nicolasparada.netlify.com/posts/go-messenger-home-page/

作者:Nicolás Parada 选题:lujun9972 译者:gxlct008 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

无论你的脚本是否成功运行, 信号捕获 trap 都能让它平稳结束。

Shell 脚本的启动并不难被检测到,但 Shell 脚本的终止检测却并不容易,因为我们无法确定脚本会按照预期地正常结束,还是由于意外的错误导致失败。当脚本执行失败时,将正在处理的内容记录下来是非常有用的做法,但有时候这样做起来并不方便。而 Bashtrap 命令的存在正是为了解决这个问题,它可以捕获到脚本的终止信号,并以某种预设的方式作出应对。

响应失败

如果出现了一个错误,可能导致发生一连串错误。下面示例脚本中,首先在 /tmp 中创建一个临时目录,这样可以在临时目录中执行解包、文件处理等操作,然后再以另一种压缩格式进行打包:

#!/usr/bin/env bash
CWD=`pwd`
TMP=${TMP:-/tmp/tmpdir}

## create tmp dir
mkdir "${TMP}"

## extract files to tmp
tar xf "${1}" --directory "${TMP}"

## move to tmpdir and run commands
pushd "${TMP}"
for IMG in *.jpg; do
  mogrify -verbose -flip -flop "${IMG}"
done
tar --create --file "${1%.*}".tar *.jpg

## move back to origin
popd

## bundle with bzip2
bzip2 --compress "${TMP}"/"${1%.*}".tar \
      --stdout > "${1%.*}".tbz

## clean up
/usr/bin/rm -r /tmp/tmpdir

一般情况下,这个脚本都可以按照预期执行。但如果归档文件中的文件是 PNG 文件而不是期望的 JPEG 文件,脚本就会在中途失败,这时候另一个问题就出现了:最后一步删除临时目录的操作没有被正常执行。如果你手动把临时目录删掉,倒是不会造成什么影响,但是如果没有手动把临时目录删掉,在下一次执行这个脚本的时候,它必须处理一个现有的临时目录,里面充满了不可预知的剩余文件。

其中一个解决方案是在脚本开头增加一个预防性删除逻辑用来处理这种情况。但这种做法显得有些暴力,而我们更应该从结构上解决这个问题。使用 trap 是一个优雅的方法。

使用 trap 捕获信号

我们可以通过 trap 捕捉程序运行时的信号。如果你使用过 kill 或者 killall 命令,那你就已经使用过名为 SIGTERM 的信号了。除此以外,还可以执行 trap -ltrap --list 命令列出其它更多的信号:

$ trap --list
 1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP
 6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1
11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR
31) SIGSYS      34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX

可以被 trap 识别的信号除了以上这些,还包括:

  • EXIT:进程退出时发出的信号
  • ERR:进程以非 0 状态码退出时发出的信号
  • DEBUG:表示调试模式的布尔值

如果要在 Bash 中实现信号捕获,只需要在 trap 后加上需要执行的命令,再加上需要捕获的信号列表就可以了。

例如,下面的这行语句可以捕获到在进程运行时用户按下 Ctrl + C 组合键发出的 SIGINT 信号:

trap "{ echo 'Terminated with Ctrl+C'; }" SIGINT

因此,上文中脚本的缺陷可以通过使用 trap 捕获 SIGINTSIGTERM、进程错误退出、进程正常退出等信号,并正确处理临时目录的方式来修复:

#!/usr/bin/env bash
CWD=`pwd`
TMP=${TMP:-/tmp/tmpdir}

trap \
 "{ /usr/bin/rm -r "${TMP}" ; exit 255; }" \
 SIGINT SIGTERM ERR EXIT

## create tmp dir
mkdir "${TMP}"
tar xf "${1}" --directory "${TMP}"

## move to tmp and run commands
pushd "${TMP}"
for IMG in *.jpg; do
  mogrify -verbose -flip -flop "${IMG}"
done
tar --create --file "${1%.*}".tar *.jpg

## move back to origin
popd

## zip tar
bzip2 --compress $TMP/"${1%.*}".tar \
      --stdout > "${1%.*}".tbz

对于更复杂的功能,还可以用 Bash 函数来简化 trap 语句。

Bash 中的信号捕获

信号捕获可以让脚本在无论是否成功执行所有任务的情况下都能够正确完成清理工作,能让你的脚本更加可靠,这是一个很好的习惯。尽管尝试把信号捕获加入到你的脚本里看看能够起到什么作用吧。


via: https://opensource.com/article/20/6/bash-trap

作者:Seth Kenlon 选题:lujun9972 译者:HankChow 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

本文是该系列的第七篇。

现在我们已经完成了后端,让我们转到前端。 我将采用单页应用程序方案。

首先,我们创建一个 static/index.html 文件,内容如下。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Messenger</title>
    <link rel="shortcut icon" href="data:,">
    <link rel="stylesheet" href="/styles.css">
    <script src="/main.js" type="module"></script>
</head>
<body></body>
</html>

这个 HTML 文件必须为每个 URL 提供服务,并且使用 JavaScript 负责呈现正确的页面。

因此,让我们将注意力转到 main.go 片刻,然后在 main() 函数中添加以下路由:

router.Handle("GET", "/...", http.FileServer(SPAFileSystem{http.Dir("static")}))

type SPAFileSystem struct {
    fs http.FileSystem
}

func (spa SPAFileSystem) Open(name string) (http.File, error) {
    f, err := spa.fs.Open(name)
    if err != nil {
        return spa.fs.Open("index.html")
    }
    return f, nil
}

我们使用一个自定义的文件系统,因此它不是为未知的 URL 返回 404 Not Found,而是转到 index.html

路由器

index.html 中我们加载了两个文件:styles.cssmain.js。我把样式留给你自由发挥。

让我们移动到 main.js。 创建一个包含以下内容的 static/main.js 文件:

import { guard } from './auth.js'
import Router from './router.js'

let currentPage
const disconnect = new CustomEvent('disconnect')
const router = new Router()

router.handle('/', guard(view('home'), view('access')))
router.handle('/callback', view('callback'))
router.handle(/^\/conversations\/([^\/]+)$/, guard(view('conversation'), view('access')))
router.handle(/^\//, view('not-found'))

router.install(async result => {
    document.body.innerHTML = ''
    if (currentPage instanceof Node) {
        currentPage.dispatchEvent(disconnect)
    }
    currentPage = await result
    if (currentPage instanceof Node) {
        document.body.appendChild(currentPage)
    }
})

function view(pageName) {
    return (...args) => import(`/pages/${pageName}-page.js`)
        .then(m => m.default(...args))
}

如果你是这个博客的关注者,你已经知道它是如何工作的了。 该路由器就是在 这里 显示的那个。 只需从 @nicolasparada/router 下载并保存到 static/router.js 即可。

我们注册了四条路由。 在根路由 / 处,我们展示 homeaccess 页面,无论用户是否通过身份验证。 在 /callback 中,我们展示 callback 页面。 在 /conversations/{conversationID} 上,我们展示对话或 access 页面,无论用户是否通过验证,对于其他 URL,我们展示一个 not-found 页面。

我们告诉路由器将结果渲染为文档主体,并在离开之前向每个页面调度一个 disconnect 事件。

我们将每个页面放在不同的文件中,并使用新的动态 import() 函数导入它们。

身份验证

guard() 是一个函数,给它两个函数作为参数,如果用户通过了身份验证,则执行第一个函数,否则执行第二个。它来自 auth.js,所以我们创建一个包含以下内容的 static/auth.js 文件:

export function isAuthenticated() {
    const token = localStorage.getItem('token')
    const expiresAtItem = localStorage.getItem('expires\_at')
    if (token === null || expiresAtItem === null) {
        return false
    }

    const expiresAt = new Date(expiresAtItem)
    if (isNaN(expiresAt.valueOf()) || expiresAt <= new Date()) {
        return false
    }

    return true
}

export function guard(fn1, fn2) {
    return (...args) => isAuthenticated()
        ? fn1(...args)
        : fn2(...args)
}

export function getAuthUser() {
    if (!isAuthenticated()) {
        return null
    }

    const authUser = localStorage.getItem('auth\_user')
    if (authUser === null) {
        return null
    }

    try {
        return JSON.parse(authUser)
    } catch (_) {
        return null
    }
}

isAuthenticated() 检查 localStorage 中的 tokenexpires_at,以判断用户是否已通过身份验证。getAuthUser()localStorage 中获取经过身份验证的用户。

当我们登录时,我们会将所有的数据保存到 localStorage,这样才有意义。

Access 页面

access page screenshot

让我们从 access 页面开始。 创建一个包含以下内容的文件 static/pages/access-page.js

const template = document.createElement('template')
template.innerHTML = `
 <h1>Messenger</h1>
 <a href="/api/oauth/github" onclick="event.stopPropagation()">Access with GitHub</a>
`

export default function accessPage() {
    return template.content
}

因为路由器会拦截所有链接点击来进行导航,所以我们必须特别阻止此链接的事件传播。

单击该链接会将我们重定向到后端,然后重定向到 GitHub,再重定向到后端,然后再次重定向到前端; 到 callback 页面。

Callback 页面

创建包括以下内容的 static/pages/callback-page.js 文件:

import http from '../http.js'
import { navigate } from '../router.js'

export default async function callbackPage() {
    const url = new URL(location.toString())
    const token = url.searchParams.get('token')
    const expiresAt = url.searchParams.get('expires\_at')

    try {
        if (token === null || expiresAt === null) {
            throw new Error('Invalid URL')
        }

        const authUser = await getAuthUser(token)

        localStorage.setItem('auth\_user', JSON.stringify(authUser))
        localStorage.setItem('token', token)
        localStorage.setItem('expires\_at', expiresAt)
    } catch (err) {
        alert(err.message)
    } finally {
        navigate('/', true)
    }
}

function getAuthUser(token) {
    return http.get('/api/auth\_user', { authorization: `Bearer ${token}` })
}

callback 页面不呈现任何内容。这是一个异步函数,它使用 URL 查询字符串中的 token 向 /api/auth_user 发出 GET 请求,并将所有数据保存到 localStorage。 然后重定向到 /

HTTP

这里是一个 HTTP 模块。 创建一个包含以下内容的 static/http.js 文件:

import { isAuthenticated } from './auth.js'

async function handleResponse(res) {
    const body = await res.clone().json().catch(() => res.text())

    if (res.status === 401) {
        localStorage.removeItem('auth\_user')
        localStorage.removeItem('token')
        localStorage.removeItem('expires\_at')
    }

    if (!res.ok) {
        const message = typeof body === 'object' && body !== null && 'message' in body
            ? body.message
            : typeof body === 'string' && body !== ''
                ? body
                : res.statusText
        throw Object.assign(new Error(message), {
            url: res.url,
            statusCode: res.status,
            statusText: res.statusText,
            headers: res.headers,
            body,
        })
    }

    return body
}

function getAuthHeader() {
    return isAuthenticated()
        ? { authorization: `Bearer ${localStorage.getItem('token')}` }
        : {}
}

export default {
    get(url, headers) {
        return fetch(url, {
            headers: Object.assign(getAuthHeader(), headers),
        }).then(handleResponse)
    },

    post(url, body, headers) {
        const init = {
            method: 'POST',
            headers: getAuthHeader(),
        }
        if (typeof body === 'object' && body !== null) {
            init.body = JSON.stringify(body)
            init.headers['content-type'] = 'application/json; charset=utf-8'
        }
        Object.assign(init.headers, headers)
        return fetch(url, init).then(handleResponse)
    },

    subscribe(url, callback) {
        const urlWithToken = new URL(url, location.origin)
        if (isAuthenticated()) {
            urlWithToken.searchParams.set('token', localStorage.getItem('token'))
        }
        const eventSource = new EventSource(urlWithToken.toString())
        eventSource.onmessage = ev => {
            let data
            try {
                data = JSON.parse(ev.data)
            } catch (err) {
                console.error('could not parse message data as JSON:', err)
                return
            }
            callback(data)
        }
        const unsubscribe = () => {
            eventSource.close()
        }
        return unsubscribe
    },
}

这个模块是 fetchEventSource API 的包装器。最重要的部分是它将 JSON web 令牌添加到请求中。

Home 页面

home page screenshot

因此,当用户登录时,将显示 home 页。 创建一个具有以下内容的 static/pages/home-page.js 文件:

import { getAuthUser } from '../auth.js'
import { avatar } from '../shared.js'

export default function homePage() {
    const authUser = getAuthUser()
    const template = document.createElement('template')
    template.innerHTML = `
 <div>
 <div>
 ${avatar(authUser)}
 <span>${authUser.username}</span>
 </div>
 <button id="logout-button">Logout</button>
 </div>
 <!-- conversation form here -->
 <!-- conversation list here -->
 `
    const page = template.content
    page.getElementById('logout-button').onclick = onLogoutClick
    return page
}

function onLogoutClick() {
    localStorage.clear()
    location.reload()
}

对于这篇文章,这是我们在 home 页上呈现的唯一内容。我们显示当前经过身份验证的用户和注销按钮。

当用户单击注销时,我们清除 localStorage 中的所有内容并重新加载页面。

Avatar

那个 avatar() 函数用于显示用户的头像。 由于已在多个地方使用,因此我将它移到 shared.js 文件中。 创建具有以下内容的文件 static/shared.js

export function avatar(user) {
    return user.avatarUrl === null
        ? `<figure class="avatar" data-initial="${user.username[0]}"></figure>`
        : `<img class="avatar" src="${user.avatarUrl}" alt="${user.username}'s avatar">`
}

如果头像网址为 null,我们将使用用户的姓名首字母作为初始头像。

你可以使用 attr() 函数显示带有少量 CSS 样式的首字母。

.avatar[data-initial]::after {
    content: attr(data-initial);
}

仅开发使用的登录

access page with login form screenshot

在上一篇文章中,我们为编写了一个登录代码。让我们在 access 页面中为此添加一个表单。 进入 static/ages/access-page.js,稍微修改一下。

import http from '../http.js'

const template = document.createElement('template')
template.innerHTML = `
 <h1>Messenger</h1>
 <form id="login-form">
 <input type="text" placeholder="Username" required>
 <button>Login</button>
 </form>
 <a href="/api/oauth/github" onclick="event.stopPropagation()">Access with GitHub</a>
`

export default function accessPage() {
    const page = template.content.cloneNode(true)
    page.getElementById('login-form').onsubmit = onLoginSubmit
    return page
}

async function onLoginSubmit(ev) {
    ev.preventDefault()

    const form = ev.currentTarget
    const input = form.querySelector('input')
    const submitButton = form.querySelector('button')

    input.disabled = true
    submitButton.disabled = true

    try {
        const payload = await login(input.value)
        input.value = ''

        localStorage.setItem('auth\_user', JSON.stringify(payload.authUser))
        localStorage.setItem('token', payload.token)
        localStorage.setItem('expires\_at', payload.expiresAt)

        location.reload()
    } catch (err) {
        alert(err.message)
        setTimeout(() => {
            input.focus()
        }, 0)
    } finally {
        input.disabled = false
        submitButton.disabled = false
    }
}

function login(username) {
    return http.post('/api/login', { username })
}

我添加了一个登录表单。当用户提交表单时。它使用用户名对 /api/login 进行 POST 请求。将所有数据保存到 localStorage 并重新加载页面。

记住在前端完成后删除此表单。


这就是这篇文章的全部内容。在下一篇文章中,我们将继续使用主页添加一个表单来开始对话,并显示包含最新对话的列表。


via: https://nicolasparada.netlify.com/posts/go-messenger-access-page/

作者:Nicolás Parada 选题:lujun9972 译者:gxlct008 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出