Add a Go language example.

This follows the other examples so that it can be used as a tutorial,
such as the ones at:
  https://developers.google.com/protocol-buffers/docs/tutorials

Even though Go generally does not use Makefiles, I added targets for the
Go examples to be consistent with the other languages.

Edit:

Fix Travis run. Change to use $HOME instead of ~. Add protoc to path.
GOPATH entry cannot start with shell metacharacter '~': "~/gocode"

Edit(2):

Fix Go code style to address comments.
This commit is contained in:
Tim Swast 2015-11-20 15:32:53 -08:00
parent f1e14fba23
commit 7e31c4d930
8 changed files with 410 additions and 0 deletions

View File

@ -15,6 +15,7 @@ env:
- CONFIG=cpp - CONFIG=cpp
- CONFIG=cpp_distcheck - CONFIG=cpp_distcheck
- CONFIG=csharp - CONFIG=csharp
- CONFIG=golang
- CONFIG=java_jdk6 - CONFIG=java_jdk6
- CONFIG=java_jdk7 - CONFIG=java_jdk7
- CONFIG=java_oracle7 - CONFIG=java_oracle7
@ -48,6 +49,10 @@ matrix:
# which doesn't work on OS X. # which doesn't work on OS X.
- os: osx - os: osx
env: CONFIG=csharp env: CONFIG=csharp
# Requires installing golang, currently travis.sh is doing that with apt-get
# which doesn't work on OS X.
- os: osx
env: CONFIG=golang
# Add into the matrix OS X tests of Objective C (needs Xcode, so it won't # Add into the matrix OS X tests of Objective C (needs Xcode, so it won't
# work on other platforms). These are split so it doesn't take as long to run. # work on other platforms). These are split so it doesn't take as long to run.
include: include:

View File

@ -5,6 +5,8 @@
all: cpp java python all: cpp java python
cpp: add_person_cpp list_people_cpp cpp: add_person_cpp list_people_cpp
go: add_person_go list_people_go
gotest: add_person_gotest list_people_gotest
java: add_person_java list_people_java java: add_person_java list_people_java
python: add_person_python list_people_python python: add_person_python list_people_python
@ -13,6 +15,8 @@ clean:
rm -f javac_middleman AddPerson*.class ListPeople*.class com/example/tutorial/*.class rm -f javac_middleman AddPerson*.class ListPeople*.class com/example/tutorial/*.class
rm -f protoc_middleman addressbook.pb.cc addressbook.pb.h addressbook_pb2.py com/example/tutorial/AddressBookProtos.java rm -f protoc_middleman addressbook.pb.cc addressbook.pb.h addressbook_pb2.py com/example/tutorial/AddressBookProtos.java
rm -f *.pyc rm -f *.pyc
rm -f protoc_middleman_go tutorial/*.pb.go add_person_go list_people_go
rmdir tutorial 2>/dev/null || true
rmdir com/example/tutorial 2>/dev/null || true rmdir com/example/tutorial 2>/dev/null || true
rmdir com/example 2>/dev/null || true rmdir com/example 2>/dev/null || true
rmdir com 2>/dev/null || true rmdir com 2>/dev/null || true
@ -21,6 +25,11 @@ protoc_middleman: addressbook.proto
protoc --cpp_out=. --java_out=. --python_out=. addressbook.proto protoc --cpp_out=. --java_out=. --python_out=. addressbook.proto
@touch protoc_middleman @touch protoc_middleman
protoc_middleman_go: addressbook.proto
mkdir tutorial # make directory for go package
protoc --go_out=tutorial addressbook.proto
@touch protoc_middleman_go
add_person_cpp: add_person.cc protoc_middleman add_person_cpp: add_person.cc protoc_middleman
pkg-config --cflags protobuf # fails if protobuf is not installed pkg-config --cflags protobuf # fails if protobuf is not installed
c++ add_person.cc addressbook.pb.cc -o add_person_cpp `pkg-config --cflags --libs protobuf` c++ add_person.cc addressbook.pb.cc -o add_person_cpp `pkg-config --cflags --libs protobuf`
@ -29,6 +38,18 @@ list_people_cpp: list_people.cc protoc_middleman
pkg-config --cflags protobuf # fails if protobuf is not installed pkg-config --cflags protobuf # fails if protobuf is not installed
c++ list_people.cc addressbook.pb.cc -o list_people_cpp `pkg-config --cflags --libs protobuf` c++ list_people.cc addressbook.pb.cc -o list_people_cpp `pkg-config --cflags --libs protobuf`
add_person_go: add_person.go protoc_middleman_go
go build -o add_person_go add_person.go
add_person_gotest: add_person_test.go add_person_go
go test add_person.go add_person_test.go
list_people_go: list_people.go protoc_middleman_go
go build -o list_people_go list_people.go
list_people_gotest: list_people.go list_people_go
go test list_people.go list_people_test.go
javac_middleman: AddPerson.java ListPeople.java protoc_middleman javac_middleman: AddPerson.java ListPeople.java protoc_middleman
javac AddPerson.java ListPeople.java com/example/tutorial/AddressBookProtos.java javac AddPerson.java ListPeople.java com/example/tutorial/AddressBookProtos.java
@touch javac_middleman @touch javac_middleman

View File

@ -27,3 +27,28 @@ These examples are part of the Protocol Buffers tutorial, located at:
* Note that on some platforms you may have to edit the Makefile and remove * Note that on some platforms you may have to edit the Makefile and remove
"-lpthread" from the linker commands (perhaps replacing it with something else). "-lpthread" from the linker commands (perhaps replacing it with something else).
We didn't do this automatically because we wanted to keep the example simple. We didn't do this automatically because we wanted to keep the example simple.
## Go ##
The Go example requires a plugin to the protocol buffer compiler, so it is not
build with all the other examples. See:
https://github.com/golang/protobuf
for more information about Go protocol buffer support.
First, install the the Protocol Buffers compiler (protoc).
Then, install the Go Protocol Buffers plugin
($GOPATH/bin must be in your $PATH for protoc to find it):
go get github.com/golang/protobuf/protoc-gen-go
Build the Go samples in this directory with "make go". This creates the
following executable files in the current directory:
add_person_go list_people_go
To run the example:
./add_person_go addressbook.data
to add a person to the protocol buffer encoded file addressbook.data. The file
is created if it does not exist. To view the data, run:
./list_people_go addressbook.data
Observe that the C++, Python, and Java examples in this directory run in a
similar way and can view/modify files created by the Go example and vice
versa.

128
examples/add_person.go Normal file
View File

@ -0,0 +1,128 @@
package main
import (
"bufio"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"strings"
"github.com/golang/protobuf/proto"
pb "github.com/google/protobuf/examples/tutorial"
)
func promptForAddress(r io.Reader) (*pb.Person, error) {
// A protocol buffer can be created like any struct.
p := &pb.Person{}
rd := bufio.NewReader(r)
fmt.Print("Enter person ID number: ")
// An int32 field in the .proto file is represented as an int32 field
// in the generated Go struct.
if _, err := fmt.Fscanf(rd, "%d\n", &p.Id); err != nil {
return p, err
}
fmt.Print("Enter name: ")
name, err := rd.ReadString('\n')
if err != nil {
return p, err
}
// A string field in the .proto file results in a string field in Go.
// We trim the whitespace because rd.ReadString includes the trailing
// newline character in its output.
p.Name = strings.TrimSpace(name)
fmt.Print("Enter email address (blank for none): ")
email, err := rd.ReadString('\n')
if err != nil {
return p, err
}
p.Email = strings.TrimSpace(email)
for {
fmt.Print("Enter a phone number (or leave blank to finish): ")
phone, err := rd.ReadString('\n')
if err != nil {
return p, err
}
phone = strings.TrimSpace(phone)
if phone == "" {
break
}
// The PhoneNumber message type is nested within the Person
// message in the .proto file. This results in a Go struct
// named using the name of the parent prefixed to the name of
// the nested message. Just as with pb.Person, it can be
// created like any other struct.
pn := &pb.Person_PhoneNumber{
Number: phone,
}
fmt.Print("Is this a mobile, home, or work phone? ")
ptype, err := rd.ReadString('\n')
if err != nil {
return p, err
}
ptype = strings.TrimSpace(ptype)
// A proto enum results in a Go constant for each enum value.
switch ptype {
case "mobile":
pn.Type = pb.Person_MOBILE
case "home":
pn.Type = pb.Person_HOME
case "work":
pn.Type = pb.Person_WORK
default:
fmt.Printf("Unknown phone type %q. Using default.\n", ptype)
}
// A repeated proto field maps to a slice field in Go. We can
// append to it like any other slice.
p.Phones = append(p.Phones, pn)
}
return p, nil
}
// Main reads the entire address book from a file, adds one person based on
// user input, then writes it back out to the same file.
func main() {
if len(os.Args) != 2 {
log.Fatalf("Usage: %s ADDRESS_BOOK_FILE\n", os.Args[0])
}
fname := os.Args[1]
// Read the existing address book.
in, err := ioutil.ReadFile(fname)
if err != nil {
if os.IsNotExist(err) {
fmt.Printf("%s: File not found. Creating new file.\n", fname)
} else {
log.Fatalln("Error reading file:", err)
}
}
book := &pb.AddressBook{}
if err := proto.Unmarshal(in, book); err != nil {
log.Fatalln("Failed to parse address book:", err)
}
// Add an address.
addr, err := promptForAddress(os.Stdin)
if err != nil {
log.Fatalln("Error with address:", err)
}
book.People = append(book.People, addr)
// Write the new address book back to disk.
out, err := proto.Marshal(book)
if err != nil {
log.Fatalln("Failed to encode address book:", err)
}
if err := ioutil.WriteFile(fname, out, 0644); err != nil {
log.Fatalln("Failed to write address book:", err)
}
}

View File

@ -0,0 +1,58 @@
package main
import (
"strings"
"testing"
"github.com/golang/protobuf/proto"
pb "github.com/google/protobuf/examples/tutorial"
)
func TestPromptForAddressReturnsAddress(t *testing.T) {
in := `12345
Example Name
name@example.com
123-456-7890
home
222-222-2222
mobile
111-111-1111
work
777-777-7777
unknown
`
got, err := promptForAddress(strings.NewReader(in))
if err != nil {
t.Fatalf("promptForAddress(%q) had unexpected error: %s", in, err.Error())
}
if got.Id != 12345 {
t.Errorf("promptForAddress(%q) got %d, want ID %d", in, got.Id, 12345)
}
if got.Name != "Example Name" {
t.Errorf("promptForAddress(%q) => want name %q, got %q", "Example Name", got.Name)
}
if got.Email != "name@example.com" {
t.Errorf("promptForAddress(%q) => want email %q, got %q", "name@example.com", got.Email)
}
want := []*pb.Person_PhoneNumber{
{Number: "123-456-7890", Type: pb.Person_HOME},
{Number: "222-222-2222", Type: pb.Person_MOBILE},
{Number: "111-111-1111", Type: pb.Person_WORK},
{Number: "777-777-7777", Type: pb.Person_MOBILE},
}
if len(got.Phones) != len(want) {
t.Errorf("want %d phone numbers, got %d", len(want), len(got.Phones))
}
phones := len(got.Phones)
if phones > len(want) {
phones = len(want)
}
for i := 0; i < phones; i++ {
if !proto.Equal(got.Phones[i], want[i]) {
t.Errorf("want phone %q, got %q", *want[i], *got.Phones[i])
}
}
}

59
examples/list_people.go Normal file
View File

@ -0,0 +1,59 @@
package main
import (
"fmt"
"io"
"io/ioutil"
"log"
"os"
"github.com/golang/protobuf/proto"
pb "github.com/google/protobuf/examples/tutorial"
)
func listPeople(w io.Writer, book *pb.AddressBook) {
for _, p := range book.People {
fmt.Fprintln(w, "Person ID:", p.Id)
fmt.Fprintln(w, " Name:", p.Name)
if p.Email != "" {
fmt.Fprintln(w, " E-mail address:", p.Email)
}
for _, pn := range p.Phones {
switch pn.Type {
case pb.Person_MOBILE:
fmt.Fprint(w, " Mobile phone #: ")
case pb.Person_HOME:
fmt.Fprint(w, " Home phone #: ")
case pb.Person_WORK:
fmt.Fprint(w, " Work phone #: ")
}
fmt.Fprintln(w, pn.Number)
}
}
}
// Main reads the entire address book from a file and prints all the
// information inside.
func main() {
if len(os.Args) != 2 {
log.Fatalf("Usage: %s ADDRESS_BOOK_FILE\n", os.Args[0])
}
fname := os.Args[1]
// Read the existing address book.
in, err := ioutil.ReadFile(fname)
if err != nil {
if os.IsNotExist(err) {
fmt.Printf("%s: File not found. Creating new file.\n", fname)
} else {
log.Fatalln("Error reading file:", err)
}
}
book := &pb.AddressBook{}
if err := proto.Unmarshal(in, book); err != nil {
log.Fatalln("Failed to parse address book:", err)
}
listPeople(os.Stdout, book)
}

View File

@ -0,0 +1,96 @@
package main
import (
"bytes"
"strings"
"testing"
pb "github.com/google/protobuf/examples/tutorial"
)
func TestListPeopleWritesList(t *testing.T) {
buf := new(bytes.Buffer)
in := pb.AddressBook{[]*pb.Person{
{
Name: "John Doe",
Id: 101,
Email: "john@example.com",
},
{
Name: "Jane Doe",
Id: 102,
},
{
Name: "Jack Doe",
Id: 201,
Email: "jack@example.com",
Phones: []*pb.Person_PhoneNumber{
{Number: "555-555-5555", Type: pb.Person_WORK},
},
},
{
Name: "Jack Buck",
Id: 301,
Email: "buck@example.com",
Phones: []*pb.Person_PhoneNumber{
{Number: "555-555-0000", Type: pb.Person_HOME},
{Number: "555-555-0001", Type: pb.Person_MOBILE},
{Number: "555-555-0002", Type: pb.Person_WORK},
},
},
{
Name: "Janet Doe",
Id: 1001,
Email: "janet@example.com",
Phones: []*pb.Person_PhoneNumber{
{Number: "555-777-0000"},
{Number: "555-777-0001", Type: pb.Person_HOME},
},
},
}}
listPeople(buf, &in)
want := strings.Split(`Person ID: 101
Name: John Doe
E-mail address: john@example.com
Person ID: 102
Name: Jane Doe
Person ID: 201
Name: Jack Doe
E-mail address: jack@example.com
Work phone #: 555-555-5555
Person ID: 301
Name: Jack Buck
E-mail address: buck@example.com
Home phone #: 555-555-0000
Mobile phone #: 555-555-0001
Work phone #: 555-555-0002
Person ID: 1001
Name: Janet Doe
E-mail address: janet@example.com
Mobile phone #: 555-777-0000
Home phone #: 555-777-0001
`, "\n")
got := strings.Split(buf.String(), "\n")
if len(got) != len(want) {
t.Errorf(
"listPeople(%s) =>\n\t%q has %d lines, want %d",
in.String(),
buf.String(),
len(got),
len(want))
}
lines := len(got)
if lines > len(want) {
lines = len(want)
}
for i := 0; i < lines; i++ {
if got[i] != want[i] {
t.Errorf(
"listPeople(%s) =>\n\tline %d %q, want %q",
in.String(),
i,
got[i],
want[i])
}
}
}

View File

@ -47,6 +47,24 @@ build_csharp() {
cd conformance && make test_csharp && cd .. cd conformance && make test_csharp && cd ..
} }
build_golang() {
# Go build needs `protoc`.
internal_build_cpp
# Add protoc to the path so that the examples build finds it.
export PATH="`pwd`/src:$PATH"
# Install Go and the Go protobuf compiler plugin.
sudo apt-get update -qq
sudo apt-get install -qq golang
export GOPATH="$HOME/gocode"
mkdir -p "$GOPATH/src/github.com/google"
ln -s "`pwd`" "$GOPATH/src/github.com/google/protobuf"
export PATH="$GOPATH/bin:$PATH"
go get github.com/golang/protobuf/protoc-gen-go
cd examples && make gotest && cd ..
}
use_java() { use_java() {
version=$1 version=$1
case "$version" in case "$version" in